Database

Database#

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: pendingprovisioningready|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[]; typestring,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.

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 silenciososapply/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.