# Bases de datos PostgreSQL

Hay dos tipos de base de datos en Zaroz y la misma API lee ambos.

| | Base de datos incluida | PostgreSQL dedicado |
|---|---|---|
| Qué es | Una base de datos en un servidor compartido, incluida con una suscripción | Un servidor PostgreSQL propio, vendido como producto con su plan y volumen |
| Coste | Incluido: **diez** bases de **64 MiB** por suscripción | Se factura por su plan (CPU, memoria, almacenamiento), como cualquier servicio |
| Aislamiento | Servidor compartido, tu propia base y usuario | Tu propia instancia y credenciales |
| Límite de almacenamiento | Una cuota (64 MiB); bloquea la escritura al superarla | Lo marca su propio volumen; sin cuota |
| Se crea con | `POST /subscriptions/{id}/shared-databases` | Comprando el producto PostgreSQL ([Suscripciones y facturación](/es/concepts/billing)) |

Un PostgreSQL dedicado ejecuta una versión mayor de PostgreSQL fija y anclada, que la consola muestra en el servicio. Zaroz no promete ninguna extensión concreta: usa lo que trae esa versión y consulta con soporte antes de depender de otra cosa. El volumen de una base dedicada se puede copiar y restaurar como el de cualquier servicio; consulta [Copias de seguridad y restauración](/es/guides/backups/overview).

Los ejemplos usan el ayudante de [Usar la API de Zaroz](/es/guides/api/overview).

:::note
Un despliegue puede ofrecer menos funciones de base de datos que otro. Pregunta [qué ofrece](/es/guides/api/overview#qué-ofrece-un-despliegue) y espera `501 capability-not-available` para lo que falte.
:::

## Crear una base de datos incluida

Comprueba lo que te queda y qué motores aloja este despliegue, y crea una.

**curl**

```bash
curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/shared-databases/entitlement" -H "$AUTH"
curl -sS "$ZAROZ_API/subscriptions/$SUBSCRIPTION/shared-databases/engines" -H "$AUTH"
# {"engines": ["postgres", ...]}

curl -sS -X POST "$ZAROZ_API/subscriptions/$SUBSCRIPTION/shared-databases" -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"engine": "postgres", "display_name": "app-db"}'
```

```json
{
  "data": {
    "id": "7c0f…",
    "display_name": "app-db",
    "engine": "postgres",
    "database": "db_3f9a…",
    "username": "u_3f9a…",
    "password": "…",
    "external_host": "db.example.zaroz.cloud",
    "external_port": 5432,
    "internal_host": "…",
    "internal_port": 5432,
    "quota_bytes": 67108864,
    "included": true
  }
}
```

**TypeScript**

```ts
import { call } from "./zaroz";

const ent = await call("GET", `/subscriptions/${SUB}/shared-databases/entitlement`);
console.log(ent.body);

const db = await call("POST", `/subscriptions/${SUB}/shared-databases`, {
  engine: "postgres",
  display_name: "app-db",
});
const d = db.body.data;
console.log(`postgresql://${d.username}:${d.password}@${d.external_host}:${d.external_port}/${d.database}`);
const DATABASE_ID = d.id;
```

**Python**

```python
from zaroz import call

_, ent = call("GET", f"/subscriptions/{SUB}/shared-databases/entitlement")
print(ent)

_, db = call("POST", f"/subscriptions/{SUB}/shared-databases", {"engine": "postgres", "display_name": "app-db"})
d = db["data"]
print(f"postgresql://{d['username']}:{d['password']}@{d['external_host']}:{d['external_port']}/{d['database']}")
DATABASE_ID = d["id"]
```

- Los campos de la respuesta son todo lo que necesita un cliente: `external_host` y `external_port` desde internet, `internal_host` e `internal_port` desde dentro de Zaroz (úsalos desde un Workload de la misma región cuando `internal_connection_available` es verdadero).
- No puedes tener más de diez: la undécima responde `429 quota-exceeded`. `region_id` es opcional; si lo das y ningún host sirve esa región y motor, la respuesta es `503` en lugar de colocarla en otro sitio.
- La suscripción debe estar activa (`422` si no), y crear requiere la autoridad para comprar por la organización (Owner o Admin).
- `GET /subscriptions/{id}/shared-databases` las lista y muestra la **contraseña** de cada una, así que trata esa respuesta como secreta.
- Rota la contraseña con `POST .../shared-databases/{database_id}/rotate-credentials` (solo cambia la contraseña) y borra con `DELETE .../shared-databases/{database_id}`: el hueco se libera al instante y los datos permanecen un breve periodo de recuperación en el host antes de eliminarse.

## Conectar con una base dedicada

Un PostgreSQL dedicado es una instancia de servicio. Su usuario y contraseña se eligen en su configuración: lee las respuestas con `GET /service-instances/{id}/setup` y, para una contraseña que necesites volver a ver, la ruta de revelado (`POST /service-instances/{id}/setup/steps/server_passwords/{password_id}/reveal`), que queda registrada en el registro de actividad. Puedes abrirle un puerto público como a cualquier servicio ([Puertos públicos y endpoints](/es/units/endpoints)); mantenlo cerrado salvo que lo necesites desde fuera.

## El id de la base de datos

Todo lo siguiente se direcciona con un id:

- para una base **incluida**, el `id` de la base (el `id` de la respuesta de creación);
- para una base **dedicada**, el **id de instancia de servicio** del servicio PostgreSQL.

Las rutas son `/databases/{database_id}/...`. Leer requiere acceso a la suscripción; **cambiar** cosas requiere el rol Owner o Admin (un miembro normal puede explorar pero no cambiar). Un id que no puedes usar responde `404`, igual que uno que no existe.

## Listar tablas

**curl**

```bash
curl -sS "$ZAROZ_API/databases/$DATABASE_ID/tables" -H "$AUTH"
curl -sS "$ZAROZ_API/databases/$DATABASE_ID/tables/users" -H "$AUTH"
```

**TypeScript**

```ts
const tables = await call("GET", `/databases/${DATABASE_ID}/tables`);
console.log(tables.body.data.map((t: any) => t.name));
```

**Python**

```python
_, tables = call("GET", f"/databases/{DATABASE_ID}/tables")
print([t["name"] for t in tables["data"]])
```

Cada tabla lista sus columnas, su clave primaria y estimaciones de tamaño. Los tamaños (`row_estimate`, `data_length`, `index_length`) son estimaciones del motor, no recuentos; muéstralos como aproximaciones.

## Leer filas

Las páginas usan cursor. `limit` es 50 por defecto y se **recorta** a 500, así que mira el `limit` de la respuesta. Devuelve `next_cursor` como `cursor`; nunca construyas un cursor tú. `sort` es `column`, `column:asc` o `column:desc`. `filters` es un array JSON de `{column, operator, value}`, donde `operator` es `equals`, `not_equals`, `contains`, `starts_with`, `ends_with`, `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal`, `is_null` o `is_not_null` (los dos últimos no llevan `value`). Un `sort` o `filters` que no se pueda interpretar o nombre una columna desconocida se rechaza, nunca se ignora en silencio.

**curl**

```bash
curl -sS -G "$ZAROZ_API/databases/$DATABASE_ID/tables/users/rows" -H "$AUTH" \
  --data-urlencode 'limit=100' --data-urlencode 'sort=created_at:desc' \
  --data-urlencode 'filters=[{"column":"email","operator":"ends_with","value":"@example.com"}]'
```

**TypeScript**

```ts
const filters = JSON.stringify([{ column: "email", operator: "ends_with", value: "@example.com" }]);
const q = new URLSearchParams({ limit: "100", sort: "created_at:desc", filters });
const page = await call("GET", `/databases/${DATABASE_ID}/tables/users/rows?${q}`);
console.log(page.body.data);
```

**Python**

```python
import json
from urllib.parse import urlencode

filters = json.dumps([{"column": "email", "operator": "ends_with", "value": "@example.com"}])
q = urlencode({"limit": 100, "sort": "created_at:desc", "filters": filters})
_, page = call("GET", f"/databases/{DATABASE_ID}/tables/users/rows?{q}")
print(page["data"])
```

## Cambiar datos y estructura

`POST` inserta una fila (`values` asigna valor a cada columna), `PATCH` actualiza una y `DELETE` borra una, localizada por el valor de la **clave primaria de una sola columna** de la tabla: una tabla sin clave primaria, o con una compuesta, no se puede editar así (usa SQL). Crea una tabla con tipos de columna neutros (`integer`, `decimal`, `float`, `text`, `boolean`, `date_time`, `date`, `time`, `json`, `binary`, `uuid`); `PATCH /tables/{name}` añade, elimina, renombra o modifica columnas o renombra la tabla; `DELETE /tables/{name}` la elimina, **para siempre**. Cada una se valida contra las columnas reales de la tabla.

## Ejecutar SQL

Una sentencia por llamada, ejecutada **como el usuario propio de la base de datos**, cancelada a los 15 segundos y registrada en el registro de actividad de la organización. Una sentencia que devuelve filas responde `{"kind": "rows", "columns", "rows", "truncated"}` (como máximo 200 filas); cualquier otra responde `{"kind": "command", "affected_rows"}`.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/databases/$DATABASE_ID/sql" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"sql": "SELECT id, email FROM users ORDER BY id LIMIT 10"}'
```

**TypeScript**

```ts
const r = await call("POST", `/databases/${DATABASE_ID}/sql`, { sql: "SELECT count(*) AS n FROM users" });
if (r.body.data.kind === "rows") console.log(r.body.data.rows);
```

**Python**

```python
_, r = call("POST", f"/databases/{DATABASE_ID}/sql", {"sql": "SELECT count(*) AS n FROM users"})
if r["data"]["kind"] == "rows":
    print(r["data"]["rows"])
```

Para algo más grande que una consulta, conéctate con `psql` o con el controlador de tu aplicación: este endpoint es para inspeccionar y para arreglos pequeños.

## Almacenamiento y cuota

**curl**

```bash
curl -sS "$ZAROZ_API/databases/$DATABASE_ID/usage" -H "$AUTH"
# {"database": "...", "data": {"used_bytes": 1048576, "data_length": ..., "index_length": ..., "quota_bytes": 67108864, "over_quota_since": null}}
```

Una base incluida tiene cuota; una dedicada devuelve `quota_bytes: null` y su tamaño lo marca el volumen. Cuando una base incluida supera la cuota queda **bloqueada en solo lectura** (`over_quota_since` se rellena) y todo lo que la haría crecer (insertar, actualizar, crear tabla, SQL en bruto) responde `507`. **Borrar** se permite siempre, incluso eliminar tablas: borra datos y el bloqueo se levanta. La cifra se guarda hasta un minuto y se refresca con cualquier escritura hecha mediante esta API.

## Exportar, importar y subir (bases incluidas)

Mover datos dentro y fuera de una base incluida se ejecuta como una operación en cola: la llamada responde **`202` con un id de operación**, no el resultado. Consulta la operación hasta que salga de `pending`/`processing` (`completed`, o `dead_letter` cuando se agotan los reintentos) y entonces descarga.

- **Exportar** (`POST .../transfers/export`) escribe un volcado JSON solo de los **datos de las filas**, nunca las sentencias `CREATE TABLE`. Está disponible para PostgreSQL.
- **Importar** (`POST .../transfers/import`, el cuerpo indica el `source_operation_id` de una exportación de la misma suscripción) restaura una exportación anterior, posiblemente en otra base de la suscripción. Las tablas deben existir ya con los mismos nombres y columnas; una fila de una tabla que no existe hace fallar toda la operación.
- **Subir** (`POST .../transfers/upload`, el archivo en bruto como cuerpo) importa un **volcado SQL**: SQL plano o comprimido (`.sql.gz`), `pg_dump --format=plain` (incluidos `COPY ... FROM stdin`), hasta 256 MiB. Un archivo `pg_dump -Fc`, vacío o binario da `422`. Las sentencias se ejecutan como el usuario de la base en orden y **se detienen en el primer error**, así que una importación fallida puede dejar aplicadas las sentencias anteriores. Se omiten `USE`, `CREATE|DROP|ALTER DATABASE` y `ALTER ... OWNER TO`.

**curl**

```bash
T=/subscriptions/$SUBSCRIPTION/shared-databases/$DATABASE_ID/transfers

# un volcado SQL directamente
curl -sS -X POST "$ZAROZ_API$T/upload" -H "$AUTH" -H "Content-Type: application/octet-stream" \
  --data-binary @dump.sql.gz
# 202 {"operation_id": "...", "status": "pending", ...}

curl -sS "$ZAROZ_API$T/$OPERATION" -H "$AUTH"                         # consultar
curl -sS -X POST "$ZAROZ_API$T/export" -H "$AUTH"                      # 202
curl -sS -o export.json "$ZAROZ_API$T/$EXPORT_OPERATION/download" -H "$AUTH"
```

**TypeScript**

```ts
import { readFile } from "node:fs/promises";

const T = `/subscriptions/${SUB}/shared-databases/${DATABASE_ID}/transfers`;
const up = await fetch(`${process.env.ZAROZ_API}${T}/upload`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.ZAROZ_TOKEN}`, "Content-Type": "application/octet-stream" },
  body: await readFile("dump.sql.gz"),
});
const { operation_id } = await up.json();

for (;;) {
  const op = await call("GET", `${T}/${operation_id}`);
  if (op.body.status === "completed" || op.body.status === "dead_letter") {
    console.log(op.body.status, op.body.last_error);
    break;
  }
  await new Promise((r) => setTimeout(r, 3000));
}
```

**Python**

```python
import os, time, requests
from zaroz import call

T = f"/subscriptions/{SUB}/shared-databases/{DATABASE_ID}/transfers"
with open("dump.sql.gz", "rb") as f:
    up = requests.post(os.environ["ZAROZ_API"] + T + "/upload", data=f, timeout=600, headers={
        "Authorization": "Bearer " + os.environ["ZAROZ_TOKEN"],
        "Content-Type": "application/octet-stream",
    })
operation_id = up.json()["operation_id"]

while True:
    _, op = call("GET", f"{T}/{operation_id}")
    if op["status"] in ("completed", "dead_letter"):
        print(op["status"], op["last_error"])
        break
    time.sleep(3)
```

Consultar es siempre seguro: nunca molesta al worker. La importación se detiene, como fallida, si llena la cuota mientras restaura.

## Qué hace distinto una base dedicada

La cuota, los límites de `usage` y las rutas de transferencia anteriores pertenecen a las bases **incluidas**. En una base dedicada, mueve los datos con `pg_dump` y `psql` por un puerto público, y apóyate en las [copias de seguridad del servicio](/es/guides/backups/overview) para recuperar. Las rutas de explorador y SQL funcionan en ambas.
