--- name: catcher-data-database description: Use quando um app ou agente precisa consumir o Database do Catcher Data (/v1/clouddb/*). MySQL 8 e PostgreSQL 16 gerenciados — provisionamento keyless, SQL guardado, schema versionado, backups/PITR, self-tuning, DBA autônomo. homepage: https://data.catcher.one/produtos/database docs: https://data.catcher.one/docs/database api_base: https://data-api.catcher.one --- # Catcher Data — Database Autenticação: `X-API-Key: ctc_…` (owner/admin). Base: https://data-api.catcher.one. Onboarding de key e conceitos gerais: https://data.catcher.one/skill. Referência navegável: https://data.catcher.one/docs/database. # Database — Cloud SQL gerenciado (`clouddb`) > Rotas `/v1/clouddb/*` · owner/admin · gated por `CLOUDDB_ENABLED` (off → 404) ## O que é Um **banco relacional gerenciado** (PostgreSQL ou MySQL, em cima do Google Cloud SQL) que você provisiona e opera **pela API**, sem tocar no console da GCP. Além do banco em si, o módulo te dá: - um **SQL editor guardado** (roda uma instrução por vez, default read-only), - um **schema declarativo versionado** (descreve o schema uma vez, aplica com diff + confirmação para mudanças destrutivas + rollback), - **multi-tenancy de end-customers** (isola os clientes do SEU cliente por linha ou por schema), e - **billing** (custo GCP + markup). ## O que dá pra fazer - Provisionar instâncias Cloud SQL (`postgres`/`mysql`) por projeto. - Rodar SQL com um guard de segurança (default-deny: sem statements empilhados, sem comentários; write/DDL só com flag explícita) + histórico. - Declarar o schema (IR `catcher.schema.v1`), pré-visualizar o diff, aplicar, versionar e fazer rollback. - Registrar/listar/remover **end-customers** (as empresas do seu cliente), com isolamento `row_level` ou `schema_per_company`. - Consultar custo/faturamento por projeto. ## Como funciona por dentro (na prática) - **Control-plane.** A API guarda metadados (instâncias, projetos, versões de schema, end-customers) no banco do seu tenant; o banco de dados real fica na GCP. - **Provisionamento assíncrono + keyless.** `POST .../instances` responde na hora com `pending` e enfileira a criação fora do request path. Os adapters GCP reais (Cloud SQL Admin + Resource Manager) rodam com **credencial keyless** (ADC da VM, sem chave guardada). Você faz poll até `ready`. - **SQL editor keyless.** As queries conectam via Cloud SQL Connector + IAM DB auth — nenhuma senha é armazenada. O guard classifica o verbo (`read`/`write`/ `ddl`) e grava o `kind` no histórico. - **Schema engine.** Valida a IR → introspecta o estado atual → planeja o diff (consciente de MySQL 8 vs Postgres) → aplica → grava uma versão **forward-only** (rollback = reaplicar uma versão antiga como uma versão nova). Drops exigem `confirm: true` e vêm com um relatório de impacto (tabelas/colunas dropadas + contagem estimada de linhas). - **Tenancy de end-customers.** O `apply` roteia pela `strategy` do projeto: `row_level` injeta `company_id` nos models `@scope(company)`; `schema_per_company` cria um schema/db por cliente sob demanda (`catcher_c_<…>`), com período de carência antes de dropar na remoção. ## Fluxo típico (end-to-end) ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' # 1. Projeto (uma vez) PID=$(curl -sX POST $B/v1/clouddb/projects -H "$KEY" -H "$JS" \ -d '{"name":"Produção","hosting_mode":"catcher","strategy":"row_level"}' | jq -r .id) # 2. Provisiona a instância (assíncrono) IID=$(curl -sX POST $B/v1/clouddb/instances -H "$KEY" -H "$JS" \ -d "{\"project_id\":\"$PID\",\"engine\":\"postgres\",\"version\":\"POSTGRES_16\",\"region\":\"southamerica-east1\",\"tier\":\"db-custom-2-7680\",\"name\":\"app-db\"}" | jq -r .id) # 3. Poll até ready curl -s $B/v1/clouddb/instances -H "$KEY" | jq '.instances[] | select(.id=="'$IID'") | .status' # 4. Declara o schema (preview → apply) curl -sX POST $B/v1/clouddb/instances/$IID/schema/apply -H "$KEY" -H "$JS" -d '{ "ir": { "format_version":"catcher.schema.v1", "models":[ {"name":"User","table":"users","scope":"global","fields":[ {"name":"id","type":"uuid","primary":true}, {"name":"email","type":"string","unique":true,"size":255}]}]}}' # 5. Roda uma query curl -sX POST $B/v1/clouddb/instances/$IID/query -H "$KEY" -H "$JS" \ -d '{"sql":"SELECT * FROM users LIMIT 10"}' ``` ## Referência de endpoints ### Projetos | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/projects` | Cria projeto (`name`, `hosting_mode`, `strategy`) → `201` | | `GET` | `/v1/clouddb/projects` | Lista projetos | ### Instâncias | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/instances` | Provisiona (`project_id`+`engine`+`version`+`region`+`tier`+`name`) → `202 {id,status:"pending"}` | | `GET` | `/v1/clouddb/instances` | Lista (`status`: `pending`→`provisioning`→`ready`\|`failed`, `last_error`) | `name` da instância: `^[a-z][a-z0-9-]{0,62}$`. `engine`: `mysql`/`postgres`. ### SQL editor | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/instances/{id}/query` | Roda **uma** instrução. Body `{sql, allow_write?, allow_ddl?}` | | `GET` | `/v1/clouddb/instances/{id}/query/history` | Histórico (mais recente primeiro) | Sucesso: `200 {ok:true, columns, rows, row_count, truncated, duration_ms}`. Erro de query (renderizável): `200 {ok:false, error, duration_ms}`. O guard rejeita `;` empilhado, comentários e verbos não-classificados; `allow_write`/`allow_ddl` default `false`. ### Schema engine (`catcher.schema.v1`) | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/instances/{id}/schema/preview` | Valida + retorna o diff, **não aplica** | | `POST` | `/v1/clouddb/instances/{id}/schema/apply` | Aplica + grava versão (`confirm:true` se destrutivo) | | `GET` | `/v1/clouddb/instances/{id}/schema/versions` | Versões (mais recente primeiro) | | `POST` | `/v1/clouddb/instances/{id}/schema/rollback` | Reaplica uma versão antiga como nova (`version`, `confirm`) | IR: `models[]` com `fields[]`; `type` ∈ `string,text,int,uint,decimal,bool,date,datetime,uuid,json`; `string` sem `size` → 255 (> 65535 → use `text`); `scope` `global`|`company`. Destrutivo → `409 SCHEMA_DESTRUCTIVE_REQUIRES_CONFIRM` (com `diff` + `impact`); IR inválida → `422 SCHEMA_INVALID` (com `issues`). ### End-customers (multi-tenancy) | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/projects/{pid}/companies` | Registra end-customer (`external_id`, `name`) → `201` | | `GET` | `/v1/clouddb/projects/{pid}/companies` | Lista (+ `strategy` + `schema_name` sob `schema_per_company`) | | `DELETE` | `/v1/clouddb/projects/{pid}/companies/{cid}` | Soft-delete (`?confirm=true` sob `schema_per_company`) → `204` | Sob `schema_per_company` a remoção mantém o schema por `CLOUDDB_COMPANY_REAP_GRACE_DAYS` (default 7) antes do reaper dropar — reversível na janela. Os "end-customers" aqui são os **Tenants** — o mesmo `external_id` (= `tenant_id`) isola esse cliente também no Vector, Storage e Cache. **Quando usar cada estratégia:** | Critério | `row_level` | `schema_per_company` | |---|---|---| | Isolamento | lógico (filtro forçado) | físico (schema separado) | | Ideal para | muitos clientes pequenos/homogêneos | separação forte, auditoria/compliance | | Custo operacional | menor (um schema só) | maior (um schema por cliente) | | Escala de nº de clientes | milhares | dezenas a centenas | A `strategy` é definida **por projeto** (imutável na criação); se precisa das duas, crie dois projetos. Visão transversal do eixo Tenant nos 4 produtos: **[Tenants](tenants.md)**. ### Billing | Método | Rota | O que faz | |---|---|---| | `GET` | `/v1/clouddb/billing/usage?from&to` | Custo GCP + faturado (markup +50% no modo `catcher`). Degrada com `available:false` se o export não estiver configurado | Money em **minor units** (centavos) + `display`, sem float drift. ## Códigos de erro do módulo `INSTANCE_NOT_FOUND` (404) · `INSTANCE_NOT_READY` (409) · `PROJECT_NOT_FOUND` (404) · `SCHEMA_INVALID` (422, com `issues`) · `SCHEMA_DESTRUCTIVE_REQUIRES_CONFIRM` (409, com `diff`+`impact`) · `SCHEMA_VERSION_NOT_FOUND` (404) · `COMPANY_ALREADY_EXISTS` (409) · `COMPANY_NOT_FOUND` (404) · `COMPANY_REMOVE_REQUIRES_CONFIRM` (409) · `MISSING_FIELD` (400). ## Gotchas - **`404` numa rota válida = `CLOUDDB_ENABLED=false`** no ambiente (ou a feature degradou por não conseguir construir os clients GCP). Não é o seu request. - **Provisionar é assíncrono** — não trate o `202` como "pronto"; faça poll até `ready`. - **Drops nunca silenciosos** — `apply`/`rollback` destrutivo só passa com `confirm:true`; leia o `impact` antes. - **SQL editor é control-plane**, uma instrução por vez — não é um pool de conexão para a sua app rodar mil queries; é para administração/inspeção.