{"openapi":"3.1.0","info":{"title":"ThinAir Data API","version":"1.0.0","description":"Connection management, schema introspection, and SQL query execution against tenant-owned databases. Both interfaces (REST and MCP) consume the same service layer; this spec describes only the REST surface."},"servers":[{"url":"https://data.thinair.co"}],"security":[{"apiKeyHeader":[]}],"components":{"securitySchemes":{"apiKeyHeader":{"type":"apiKey","in":"header","name":"x-api-key"}},"schemas":{"ProblemDetails":{"type":"object","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"code":{"type":"string","description":"Stable internal_code; see /docs/errors"},"request_id":{"type":"string"},"instance":{"type":"string"},"retry_after_ms":{"type":"integer"},"upgrade_url":{"type":["string","null"],"format":"uri","description":"Present on QUOTA_EXCEEDED (429) and TRIAL_EXPIRED (401). Where it points depends on the caller's tenant. For a tenant with no active subscription (Discover, Developer Trial, or a cancelled subscription) it is a checkout link preselecting the caller's next plan tier, tagged with `src` for the upgrade-prompt funnel. For a tenant that already has an active subscription it is the console Billing view (https://data.thinair.co/developers#billing), where the plan is changed through the Billing portal; no checkout link is issued, because a second checkout would open a second subscription. `null`/absent once there is no self-serve tier above the caller's plan."},"expired_at":{"type":["string","null"],"format":"date-time","description":"TRIAL_EXPIRED only: ISO timestamp the Developer Trial key expired."},"plan":{"type":"string","description":"TRIAL_EXPIRED only: the plan the expired key was on (normally `dev_trial`)."},"quota":{"anyOf":[{"type":"integer"},{"type":"string","enum":["unlimited"]}],"description":"TRIAL_EXPIRED only: the daily query limit the expired plan carried."}},"required":["type","title","status","detail","code","request_id","instance"],"description":"RFC 7807 problem-details with ThinAir extensions (code, request_id, retry_after_ms, upgrade_url, and — TRIAL_EXPIRED only — expired_at/plan/quota)."},"TablesProfileResponse":{"type":"object","properties":{"profiles":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"nullable":{"type":"boolean"},"isPrimaryKey":{"type":"boolean"},"null_pct":{"type":"number"},"cardinality":{"type":"number"},"min":{"anyOf":[{"type":"number"},{"type":"string"}]},"max":{"anyOf":[{"type":"number"},{"type":"string"}]},"avg":{"type":"number"},"earliest":{"type":"string"},"latest":{"type":"string"},"pii_risk":{"type":"boolean"},"pii_type":{"type":"string"}},"required":["name"]}},"summary":{"type":"string"},"insights":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string"},"message":{"type":"string"},"column":{"type":"string"},"value":{}},"required":["severity","message"]}},"meta":{"type":"object","properties":{"executionTimeMs":{"type":"number"},"rowCount":{"type":"number"},"connection":{"type":"string"},"dialect":{"type":"string","enum":["postgres","mysql","mssql"]}},"required":["executionTimeMs","rowCount","connection","dialect"]}},"required":["profiles","meta"],"description":"Per-column profile + health insights for one table. Mirrors the MCP `data_profile` envelope shape."},"TablesAnomaliesResponse":{"type":"object","properties":{"alerts":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string"},"type":{"type":"string"},"column":{"type":["string","null"]},"message":{"type":"string"},"recommendation":{"type":"string"},"value":{}},"required":["severity"]}},"summary":{"type":"string"},"insights":{"type":"array","items":{"type":"object","properties":{"severity":{"type":"string"},"message":{"type":"string"},"column":{"type":"string"},"value":{}},"required":["severity","message"]}},"meta":{"type":"object","properties":{"executionTimeMs":{"type":"number"},"connection":{"type":"string"},"dialect":{"type":"string","enum":["postgres","mysql","mssql"]}},"required":["executionTimeMs","connection","dialect"]}},"required":["alerts","meta"],"description":"Severity-ranked anomaly alerts for one table. Mirrors the MCP `detect_anomalies` envelope shape."},"OptimizeResponse":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}},"display":{"type":"object","additionalProperties":{}},"summary":{"type":"string"},"insights":{"type":"array","items":{"type":"object","additionalProperties":{}}},"meta":{"type":"object","additionalProperties":{}}},"required":["data","display","meta"],"description":"EXPLAIN-plan analysis result. `data.recommendations[]` lists each issue (sequential scan, missing index, filesort, …) with severity and a suggestion. `data.plan` includes the raw plan text only when recommendations exist; clean plans return `data.verdict: 'efficient'` plus a node-type summary instead. Mirrors the MCP `optimize_query` tool output."},"OptimizeRequest":{"type":"object","properties":{"connection":{"type":"string","minLength":1,"maxLength":256,"description":"Connection name (display) or id (uuid). When omitted, the tenant's default (first added) connection is used. Same fallback as the MCP `optimize_query` tool.","example":"fleet"},"sql":{"type":"string","minLength":1,"maxLength":50000,"description":"SQL query to analyze. Read-only (SELECT / WITH / EXPLAIN). The handler runs `EXPLAIN (ANALYZE false, FORMAT JSON)` on Postgres, `EXPLAIN` on MySQL, or `SET SHOWPLAN_XML ON` on SQL Server — an estimated plan in every case, with no execution — and parses it for sequential/table scans, nested-loop joins, and (MySQL) temporary tables and filesorts. Missing-index CREATE statements come from the SQL Server optimizer and are therefore mssql-only. Sort spills are NOT detected on any dialect: a spill is visible only in a plan produced by actually running the query, which this tool never does.","example":"SELECT * FROM drivers WHERE email = 'a@b.co'"},"dialect_hint":{"type":"string","enum":["postgres","mysql","mssql"],"description":"Optional dialect hint. The handler derives the actual dialect from the resolved connection — this hint is informational and reserved for future cross-dialect rewrite support. On SQL Server the handler captures the ESTIMATED plan via SET SHOWPLAN_XML ON/OFF (no query execution) and returns the same recommendation shape as Postgres/MySQL (scans, missing indexes, expensive operators)."}},"required":["sql"],"description":"Analyze a SQL query and return optimization suggestions: an estimated EXPLAIN plan summary plus ranked findings and index recommendations. It does not return a rewritten query — `original_query` comes back unchanged. Costs 1 quota unit."},"MigrationsGenerateResponse":{"type":"object","properties":{"data":{"type":"object","additionalProperties":{}},"display":{"type":"object","additionalProperties":{}},"summary":{"type":"string"},"insights":{"type":"array","items":{"type":"object","additionalProperties":{}}},"meta":{"type":"object","additionalProperties":{}}},"required":["data","display","meta"],"description":"Generated migration. `data.up_sql` runs the change, `data.down_sql` rolls it back, `data.warnings[]` flags caveats (PG `CONCURRENTLY` outside a tx, NULL backfill required, irreversible drops, …). Mirrors the MCP `generate_migration` tool output. Some intents emit `-- TODO:` skeletons + a parser-couldn't-resolve warning when the phrasing isn't recognized."},"MigrationsGenerateRequest":{"type":"object","properties":{"connection":{"type":"string","minLength":1,"maxLength":256,"description":"Connection name (display) or id (uuid). When omitted, the tenant's default (first added) connection is used. Same fallback as the MCP `generate_migration` tool.","example":"fleet"},"intent":{"type":"string","minLength":1,"maxLength":2000,"description":"Plain-English description of the migration. Examples: 'add soft delete to drivers', 'add index on trips(driver_id, created_at)', 'rename email to email_address on drivers'. Phrasing matters: name the table, the column, and the action explicitly — the intent parser extracts those three and will reject a request it cannot resolve rather than guess.","example":"add nullable notes column to vehicles"},"target_dialect":{"type":"string","enum":["postgres","mysql","mssql"],"description":"Optional target dialect. The handler currently derives dialect from the resolved connection — this hint is informational only and reserved for future cross-dialect translation. Mismatches between hint and resolved dialect surface in response meta but don't override."},"dry_run":{"type":"boolean","description":"Reserved for future support. The current handler NEVER executes the generated SQL — it only emits up + down strings — so dry_run is effectively always true. Echoed in response meta when set."}},"required":["intent"],"description":"Generate a SQL migration script (up + down + warnings) from a free-text intent. Costs 1 quota unit. Build tier (or higher) required."},"QuotaResponse":{"type":"object","properties":{"schema_version":{"type":"string","enum":["v1"]},"mcp":{"type":"object","properties":{"plan":{"type":"string","example":"discover"},"used_today":{"type":"integer","minimum":0},"daily_limit":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","enum":["unlimited"]}]},"remaining_today":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","enum":["unlimited"]}]},"monthly_limit":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","enum":["unlimited"]}]},"price_per_month":{"type":"string","example":"$0"},"suggested_upgrade":{"type":["string","null"]},"upgrade_url":{"type":["string","null"]},"reset_at":{"type":"string","example":"00:00 UTC daily"}},"required":["plan","used_today","daily_limit","remaining_today","monthly_limit","price_per_month","suggested_upgrade","upgrade_url","reset_at"]}},"required":["schema_version","mcp"],"description":"Caller's current quota state — plan, daily usage, remaining queries, monthly limit, and upgrade options. Mirrors geo-mcp's /v1/quota response shape so cross-product clients can share a parser. Plan-name divergence: data-mcp uses discover|build|architect|enterprise (vs geo's free|starter|pro|enterprise|admin) — treat `plan` as an opaque string."},"ConnectionRow":{"type":"object","properties":{"name":{"type":"string","description":"Display name. Use this with `/v1/connections/:id/...` (the path id is the connection name).","example":"fleet"},"id":{"type":"string","description":"Stable internal id. Useful for disambiguating duplicate-name connections; otherwise prefer `name`.","example":"conn_8f2a91c0"},"dbType":{"type":"string","enum":["postgres","mysql","mssql"],"description":"Dialect. Drives SQL syntax choice in subsequent calls."},"createdAt":{"type":"string","description":"ISO 8601 timestamp the connection was registered."}},"required":["name","id","dbType","createdAt"]},"ConnectionDuplicateWarning":{"type":"object","properties":{"code":{"type":"string","enum":["DUPLICATE_NAME"]},"name":{"type":"string"},"count":{"type":"integer","minimum":2},"message":{"type":"string"}},"required":["code","name","count","message"]},"ConnectionsListResponse":{"type":"object","properties":{"connections":{"type":"array","items":{"$ref":"#/components/schemas/ConnectionRow"}},"rowCount":{"type":"integer","minimum":0},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ConnectionDuplicateWarning"}}},"required":["connections","rowCount"],"description":"Inventory of the tenant's registered database connections. Rows include name, id, dialect, and created-at. Connection strings and encrypted material are never returned. Free to call (`X-Quota-Cost: 0`)."},"ConnectionHealthResponse":{"type":"object","properties":{"ok":{"type":"boolean","description":"True when the ping succeeded."},"db_type":{"type":"string","enum":["postgres","mysql","mssql"]},"version":{"type":"string","example":"PostgreSQL 16.2 on x86_64-pc-linux-gnu"},"current_database":{"type":["string","null"]},"current_schema":{"type":["string","null"]},"ping_ms":{"type":"integer","minimum":0,"description":"Wall time of the `SELECT 1` ping in milliseconds."},"total_ms":{"type":"integer","minimum":0,"description":"Wall time of the full probe (ping + version + schema lookup)."},"connection":{"type":"string","description":"Resolved connection name (matches the row in `/v1/connections`)."}},"required":["ok","db_type","version","current_database","current_schema","ping_ms","total_ms","connection"],"description":"Health-probe result for a single connection. Surfaces dialect, server version, current database/schema, and ping/total latency. Costs 1 quota unit per call."},"SchemaColumn":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"nullable":{"type":"boolean"},"isPrimaryKey":{"type":"boolean"},"isForeignKey":{"type":"boolean"},"referencesTable":{"type":"string"},"referencesColumn":{"type":"string"},"defaultValue":{"type":"string"}},"required":["name","type","nullable"]},"SchemaIndex":{"type":"object","properties":{"name":{"type":"string"},"columns":{"type":"array","items":{"type":"string"}},"isUnique":{"type":"boolean"},"isPrimary":{"type":"boolean"}},"required":["name","columns","isUnique","isPrimary"]},"SchemaTable":{"type":"object","properties":{"name":{"type":"string"},"columns":{"type":"array","items":{"$ref":"#/components/schemas/SchemaColumn"}},"indexes":{"type":"array","items":{"$ref":"#/components/schemas/SchemaIndex"}},"rowCountEstimate":{"type":"number"}},"required":["name","columns"]},"SchemaResult":{"type":"object","properties":{"tables":{"type":"array","items":{"$ref":"#/components/schemas/SchemaTable"}},"dbType":{"type":"string","enum":["postgres","mysql","mssql"]},"schema":{"type":"string","description":"The namespace that was introspected. Postgres: the search_path schema (e.g. \"public\"). MySQL: the database, which IS the schema there. SQL Server: present only when introspection was scoped to one schema. Absent when the connection named no database, or when every schema was swept — in which case naming one would overstate what was covered."},"notEvaluated":{"type":"array","items":{"type":"string"},"description":"Catalog queries that failed, so their annotations are MISSING rather than absent — e.g. [\"primary_keys\"] means no table in this response can be assumed to lack a primary key. Omitted when everything was evaluated."},"availableSchemas":{"type":"array","items":{"type":"object","properties":{"schema":{"type":"string"},"tableCount":{"type":"number"}},"required":["schema","tableCount"]},"description":"Populated only when the introspected schema returned 0 tables. Lists other namespaces with non-zero table counts so clients can suggest a re-introspection."}},"required":["tables","dbType"]},"ConnectionSchemaMeta":{"type":"object","properties":{"executionTimeMs":{"type":"integer","minimum":0},"rowCount":{"type":"integer","minimum":0},"cacheHit":{"type":"boolean"},"connection":{"type":"string"},"dialect":{"type":"string","enum":["postgres","mysql","mssql"]}},"required":["executionTimeMs","rowCount","connection","dialect"]},"ConnectionSchemaResponse":{"type":"object","properties":{"schema":{"$ref":"#/components/schemas/SchemaResult"},"meta":{"$ref":"#/components/schemas/ConnectionSchemaMeta"}},"required":["schema","meta"],"description":"Full schema introspection for one connection — tables, columns, primary keys, and indexes (MySQL connections do not report indexes yet; foreign keys are not reported). `cacheHit: true` when served from the 1-hour cache (sub-30ms). Costs 1 quota unit per call."},"QueryMeta":{"type":"object","properties":{"upstream_ms":{"type":"integer","minimum":0,"description":"Wall time spent inside the service-layer call (origin DB round-trip plus pool wait)."},"row_count":{"type":"integer","minimum":0,"description":"Total rows the underlying query produced (BEFORE truncation). When `truncated` is true, this is greater than `rows.length`."},"truncated":{"type":"boolean","description":"True when the result was capped at `applied_limit` rows."},"dialect":{"type":"string","enum":["postgres","mysql","mssql"],"description":"Resolved connection dialect — write follow-up SQL in this dialect's syntax (PostgreSQL `LIMIT`, T-SQL `TOP N`, MySQL `LIMIT`)."},"connection":{"type":"string","description":"Connection name (display, not id) the query ran against."},"applied_limit":{"type":"integer","exclusiveMinimum":0,"description":"Effective `limit` after clamping (default 100, max 1000)."},"cache_hit":{"type":"boolean","description":"True when the result came from the per-tenant query cache rather than the database."},"tables":{"type":"array","items":{"type":"string"},"description":"Tables referenced by the SQL (best-effort parse)."},"retries":{"type":"integer","minimum":0,"description":"Transient-retry count. Omitted on first-shot success."},"total_wait_ms":{"type":"integer","minimum":0,"description":"Cumulative wall-time spent in retry backoff. Omitted when `retries` is unset."}},"required":["upstream_ms","row_count","truncated","dialect","connection","applied_limit","tables"]},"QueryResponse":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","additionalProperties":{}},"description":"Result rows. Each row is a `{column: value}` map. `null` is used for SQL NULL; numbers, booleans, strings, arrays, and JSON objects flow through as their JS shape."},"columns":{"type":"array","items":{"type":"string"},"description":"Column names from the first row's keys (or `[]` when `rows` is empty). Service-layer `executeQuery` returns row dicts without a separate column list; deriving from the first row matches the MCP envelope's behavior."},"meta":{"$ref":"#/components/schemas/QueryMeta"}},"required":["rows","columns","meta"],"description":"Result of a SQL execution. `rows.length` may be less than `meta.row_count` when truncation applied — clients re-issue with a higher `limit` (up to 1000) to see more."},"QueryRequest":{"type":"object","properties":{"query":{"type":"string","minLength":1,"maxLength":50000,"description":"Read-only SQL statement (SELECT / WITH / EXPLAIN only). Multiple statements not allowed; sql-guard runs additional checks before execution.","example":"SELECT id, status FROM vehicles LIMIT 10"},"connection":{"type":"string","minLength":1,"maxLength":256,"description":"Connection name or id from this tenant's inventory. Falls back to the tenant's default (first added) when omitted. Mirrors the MCP `query_sql` tool's `connection` arg.","example":"fleet"},"limit":{"type":"integer","minimum":1,"maximum":1000,"description":"Max rows to return (default 100, max 1000). Mirrors `query_sql.limit`.","example":100}},"required":["query"],"description":"Execute a read-only SQL statement (SELECT / WITH / EXPLAIN). Read-only enforcement runs at three layers below this endpoint (sql-guard, session READ ONLY, optional per-connection firewall); writes are rejected upstream of the REST layer."},"QueryHistoryEntry":{"type":"object","properties":{"id":{"type":"string","description":"Stable per-entry id (UUID v4)."},"connection_id":{"type":"string","description":"Connection id the query targeted (NOT the human display name)."},"sql":{"type":"string","description":"Original SQL — may be truncated to a redacted summary (≤ 200 chars + ellipsis when longer). The MCP `query_history` tool applies the same truncation; the limit keeps history payloads bounded."},"execution_ms":{"type":"integer","minimum":0},"row_count":{"type":"integer","minimum":0},"tool":{"type":"string","description":"Tool that emitted the entry — `query_sql` for raw user queries, `explain_query` / `optimize_query` / etc. for the architect-tier tools."},"timestamp":{"type":"string","format":"date-time","description":"Wall-clock at write time, ISO 8601 (UTC)."},"tables":{"type":"array","items":{"type":"string"},"description":"Tables referenced by the SQL (best-effort parse at write time)."},"error":{"type":["string","null"],"description":"Translated error message when the query failed; `null` on success. Failed queries are still logged so a tenant can see what didn't work."}},"required":["id","connection_id","sql","execution_ms","row_count","tool","timestamp","tables","error"]},"QueryHistoryMeta":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Number of entries returned (post-filter)."},"applied_limit":{"type":"integer","exclusiveMinimum":0,"description":"Effective `limit` after clamping."},"connection":{"type":"string","description":"Echo of the `connection` filter when the caller passed one."},"since_iso":{"type":"string","description":"Echo of the `since_iso` cutoff when the caller passed one."}},"required":["count","applied_limit"]},"QueryHistoryResponse":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/QueryHistoryEntry"},"description":"Recent queries newest-first. Capped at the per-tenant ring-buffer size (100). Entries older than 7 days are dropped by the retention policy — long-term audit lives elsewhere."},"meta":{"$ref":"#/components/schemas/QueryHistoryMeta"}},"required":["entries","meta"],"description":"Per-tenant query log. Free observability call — does not debit quota and never returns 429."}},"parameters":{}},"paths":{"/v1/tables/{t}/profile":{"get":{"summary":"Profile a table","description":"Return per-column distributions, null rates, cardinality, basic numeric / date stats, and PII classification for one table. Requires the Build plan or higher. Quota: 1 capability unit.","tags":["tables"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":256,"description":"Table name. Accepts `table`, `schema.table`, or `database.schema.table` forms — same wire shape the MCP `data_profile` tool accepts. Use `?connection=` to override the tenant's default connection rather than prefixing the path.","example":"public.vehicles"},"required":true,"name":"t","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":256,"description":"Connection name (display) or id (uuid). Resolved via name match first, then id match. Call /v1/connections to list available names.","example":"fleet"},"required":false,"name":"connection","in":"query"},{"schema":{"type":"integer","minimum":100,"maximum":1000000,"description":"Advisory upper bound on rows to scan. Today the underlying profiler decides sampling internally based on table size; the value is reserved for the post-migration profile micro-spec.","example":50000},"required":false,"name":"sample_rows","in":"query"},{"schema":{"type":"string","minLength":1,"description":"Comma-separated column allowlist. Today the profiler covers the first 50 columns; the value is reserved for the post-migration profile micro-spec.","example":"make,model,year"},"required":false,"name":"columns","in":"query"}],"responses":{"200":{"description":"Profile result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TablesProfileResponse"}}}},"400":{"description":"Invalid table name or query parameters.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required or failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"The current plan does not include data_profile.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Connection not found, no connections configured, or table missing from the connection's schema.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/tables/{t}/anomalies":{"get":{"summary":"Detect anomalies in a table","description":"Scan a table for unusual patterns: volume drops/spikes, data gaps, value concentration, high null rates, stale data. Severity-ranked. Tables > 100k rows use a sampled path (~5%) — alerts may carry `sampled: true`. Quota: 1 capability unit.","tags":["tables"],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":256,"description":"Table name. Accepts `table`, `schema.table`, or `database.schema.table` forms — same wire shape the MCP `detect_anomalies` tool accepts. Use `?connection=` to override the tenant's default connection.","example":"public.telemetry_events"},"required":true,"name":"t","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":256,"description":"Connection name (display) or id (uuid). Resolved via name match first, then id match. Call /v1/connections to list available names.","example":"fleet"},"required":false,"name":"connection","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":128,"description":"Column to focus the scan on. Today maps to the date-trend column for volume / staleness anomalies; the post-migration spec broadens it to a per-column allowlist.","example":"created_at"},"required":false,"name":"column","in":"query"},{"schema":{"type":"string","format":"date-time","description":"ISO-8601 cutoff. Anomalies older than this are excluded. Today the underlying scan uses a fixed 14-day window; the value is reserved for the migrated implementation.","example":"2026-04-01T00:00:00Z"},"required":false,"name":"since_iso","in":"query"},{"schema":{"type":"string","enum":["low","medium","high"],"description":"Minimum severity to include. `low` returns everything; `medium` drops `low`; `high` drops `low` and `medium`.","example":"medium"},"required":false,"name":"severity","in":"query"}],"responses":{"200":{"description":"Anomaly scan result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TablesAnomaliesResponse"}}}},"400":{"description":"Invalid table name or query parameters.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required or failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Connection not found, no connections configured, or table missing from the connection's schema.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/query/optimize":{"post":{"summary":"Analyze a SQL query and return optimization suggestions","description":"Run EXPLAIN against the resolved connection — an estimated plan; the query is not executed — parse it, and return ranked recommendations (sequential scans, nested-loop blow-ups, MySQL filesort/temp table, and on SQL Server the optimizer's missing-index statements). Costs 1 quota unit per call. Build tier (or higher) required.","tags":["query"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OptimizeRequest"},"example":{"sql":"SELECT * FROM drivers WHERE email = 'a@b.co'"}}}},"responses":{"200":{"description":"Plan analysis. `data.recommendations` lists issues; empty + `data.verdict: 'efficient'` when nothing actionable was found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OptimizeResponse"}}}},"400":{"description":"Invalid request body or unparseable SQL.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required or failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Tier gate — requires Build plan or higher (caller is on Discover).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Connection not found, or the tenant has no connections registered.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded for this tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"502":{"description":"Upstream database unreachable (network, TLS, DNS) — no quota debited.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"Pool saturated or circuit breaker open — no quota debited.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/migrations/generate":{"post":{"summary":"Generate a SQL migration script from a free-text intent","description":"Parse a plain-English migration intent (e.g. 'add index on positions(deviceid, fixtime desc)') and emit dialect-correct up + down SQL plus warnings. Never executes — output is for the caller to review and run manually. Costs 1 quota unit per call. Build tier (or higher) required.","tags":["migrations"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrationsGenerateRequest"},"example":{"intent":"add nullable notes column to vehicles"}}}},"responses":{"200":{"description":"Generated migration. Always review `data.warnings[]` before running — some operations (drop column, create unique index) are destructive or fail when constraints aren't met.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrationsGenerateResponse"}}}},"400":{"description":"Invalid request body.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required or failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Tier gate — requires Build plan or higher (caller is on Discover).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"Connection not found, or the tenant has no connections registered.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded for this tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"502":{"description":"Upstream database unreachable during schema introspection — no quota debited.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/quota":{"get":{"summary":"Get caller's current quota state","description":"Returns the caller's plan, daily usage, remaining queries, monthly limit, and upgrade options. **Free to call** — never debits quota and never returns 429 (a quota-exhausted user must still be able to discover their state).","tags":["billing"],"responses":{"200":{"description":"Caller's quota snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaResponse"}}}},"401":{"description":"Authentication required or failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/connections":{"get":{"summary":"List the tenant's database connections","description":"Returns name, id, dialect, and created-at for every connection registered to the calling tenant. Use the `name` field as the path parameter for `/v1/connections/:id/health` and `/v1/connections/:id/schema`. **Free to call** — never debits quota.","tags":["connections"],"responses":{"200":{"description":"Connection inventory.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionsListResponse"}}}},"401":{"description":"Authentication required or the session has no tenant binding.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/connections/{id}/health":{"get":{"summary":"Probe a connection's health","description":"Run a dialect-appropriate `SELECT 1` ping and best-effort version probe against the connection identified by `id` (name match first, id match second). Returns latency, server version, current database, and current schema. Costs 1 quota unit per call.","tags":["connections"],"parameters":[{"schema":{"type":"string","description":"Connection name or id (see `/v1/connections`).","example":"fleet"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Probe completed (the connection may have reported `ok: false` for an auth or origin failure — still a 200; the body's `ok` is the success signal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionHealthResponse"}}}},"401":{"description":"Authentication required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"No connection matches `id` for this tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded for this tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"502":{"description":"Connection unreachable (origin refused / DNS / TLS / etc).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"504":{"description":"Connection probe timed out.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/connections/{id}/schema":{"get":{"summary":"Introspect a connection's schema","description":"Returns tables, columns, primary keys, and indexes (MySQL: indexes not reported yet; foreign keys are not reported) for the connection identified by `id` (name match first, id match second). Results are cached 1 hour; pass `refresh=true` to bypass the cache and force live introspection. Costs 1 quota unit per call.","tags":["connections"],"parameters":[{"schema":{"type":"string","description":"Connection name or id (see `/v1/connections`).","example":"fleet"},"required":true,"name":"id","in":"path"},{"schema":{"anyOf":[{"type":"string","enum":["true"]},{"type":"string","enum":["false"]}],"description":"Set to `true` to bypass the 1-hour cache and force live introspection. Default: `false` (use cache when present).","example":"false"},"required":false,"name":"refresh","in":"query"}],"responses":{"200":{"description":"Schema descriptor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectionSchemaResponse"}}}},"400":{"description":"Invalid query parameter.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"No connection matches `id` for this tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded for this tenant.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"502":{"description":"Upstream introspection failed (origin refused / DNS / TLS).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/query":{"post":{"summary":"Execute a read-only SQL query","description":"Runs a SELECT / WITH / EXPLAIN against the target connection. Read-only enforcement runs at three layers (sql-guard, session READ ONLY, optional per-connection firewall) below this handler — writes are rejected with `code: SYNTAX` or `PERMISSION_DENIED` and never reach the upstream DB. Costs 1 quota unit per successful call.","tags":["query"],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryRequest"},"example":{"query":"SELECT 1"}}}},"responses":{"200":{"description":"Query result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryResponse"}}}},"400":{"description":"Invalid body, SQL syntax error, or unknown table/column.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required or upstream DB auth failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"403":{"description":"Permission denied on the upstream DB (READ ONLY transaction, missing role grants).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"404":{"description":"No connections configured for tenant, or connection name not found.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"409":{"description":"Database deadlock — safe to retry (see `retry_after_ms`).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"429":{"description":"Quota exceeded, rate limited, or pool saturated.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"502":{"description":"Origin DB unreachable, refused, or TLS handshake failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"503":{"description":"Backpressure — circuit breaker open or per-tenant queue timeout.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"504":{"description":"Query exceeded the plan's `execute_timeout_ms`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}},"/v1/query/history":{"get":{"summary":"List recent queries for the calling tenant","description":"Read the tenant's per-tenant query log — recent SQL with timing, row counts, error status, and the tool that emitted each entry. The ring buffer holds up to 100 entries with a 7-day TTL. **Free to call** — never debits quota and never returns 429 (a quota-exhausted user must still be able to inspect their own activity).","tags":["query"],"parameters":[{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Max entries to return (1–100). Default 20.","example":20},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","minLength":1,"maxLength":256,"description":"Filter to entries on this connection. Matches the entry's `connectionId` (uuid) — pass the connection id, not its display name. Optional.","example":"fleet"},"required":false,"name":"connection","in":"query"},{"schema":{"type":"string","format":"date-time","description":"Drop entries older than this ISO 8601 timestamp. Useful for `?since=last_check` polling. Optional.","example":"2026-05-05T00:00:00Z"},"required":false,"name":"since_iso","in":"query"}],"responses":{"200":{"description":"Query history (newest-first).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryHistoryResponse"}}}},"400":{"description":"Invalid query parameters (out-of-range `limit`, malformed `since_iso`).","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}},"401":{"description":"Authentication required or failed.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ProblemDetails"}}}}}}}},"webhooks":{}}