🗄️ SQL custom · reportes ad-hoc
Editor SQL read-only en tu panel · ejecuta SELECT/WITH sobre tu schema de tenant · útil reports custom no cubiertos por dashboards · audit trail · sandboxed.
1. ¿Qué resuelve?
- Reports ad-hoc · pregunta específica del CFO ("¿cuántas citas canceladas en marzo por médico X?") sin esperar build dashboard.
- Data exploration · entiende patrón datos antes pedir feature · validación hipótesis comercial.
- Migration validation · post-import CSV cliente · query verifica counts + duplicados.
2. Cuándo activarlo
- Owner / admin con conocimiento SQL básico.
- Tenant con > 6 meses datos · análisis tendencia útil.
- Compliance audit · trail SELECT queries para auditor externo.
3. Setup
- Activar extra
tenant.custom_sqldesde/extras. - Ve a
/custom-sql· editor abre. - Solo owner + admin pueden ejecutar. Manager/user/viewer ven 403.
4. Restricciones obligatorias (security)
- SELECT / WITH only · INSERT · UPDATE · DELETE · DROP · ALTER bloqueados a parser level.
- 1 statement · query múltiple separada con `;` rechazada.
- Timeout 5s· query > 5s aborted automático.
- Auto-LIMIT 200· resultset truncado si > 200 filas · evita memory issues.
- Search path scoped · solo tenant schema visible (NO cross-tenant · NO public.tenants · NO public.users).
- Audit log · cada query registrada en
custom_sql_runscon actor + texto + duración + filas devueltas.
5. Flujo típico
- Editor pre-llenado con ejemplo:
SELECT id, full_name, email, created_at FROM contacts ORDER BY created_at DESC LIMIT 50 - Modificar query · ejecutar · resultados aparecen tabla.
- Guardar query · nombre + categoría + descripción · re-ejecutas con 1 click después.
- Export CSV · descarga resultados · útil compartir CFO/contador.
- Compartir · query guardada visible owner + admin · NO compartido con ningún rol inferior.
6. Queries útiles ejemplo
Top 10 clientes por revenue
SELECT c.full_name, COUNT(i.id) AS num_invoices, SUM(i.total) AS revenue
FROM contacts c
JOIN invoices i ON i.contact_id = c.id
WHERE i.status IN ('issued', 'paid')
AND i.created_at > NOW() - INTERVAL '6 months'
GROUP BY c.id, c.full_name
ORDER BY revenue DESC
LIMIT 10Citas canceladas por médico mes pasado
SELECT a.resource_id, COUNT(*) AS cancelled
FROM appointments a
WHERE a.status = 'cancelled'
AND a.starts_at >= date_trunc('month', NOW() - INTERVAL '1 month')
AND a.starts_at < date_trunc('month', NOW())
GROUP BY a.resource_id
ORDER BY cancelled DESCProductos sin venta últimos 90 días
SELECT p.id, p.sku, p.name, p.price
FROM products p
WHERE p.active = true
AND p.id NOT IN (
SELECT (jsonb_array_elements(metadata->'lines')->>'productId')::uuid
FROM invoices
WHERE created_at > NOW() - INTERVAL '90 days'
AND jsonb_typeof(metadata->'lines') = 'array'
)
ORDER BY p.name
LIMIT 507. Troubleshooting
- "permission denied for table X" · query toca tabla fuera tenant schema · refactoring requerido · custom-sql NO permite cross-tenant access.
- Timeout 5s exceeded · query lenta · agrega LIMIT más estricto · index falta · optimiza JOIN order.
- "only one statement allowed" · removí `;` · ejecuta uno a la vez.
- Resultset 200 filas hard limit · usa GROUP BY/aggregations en lugar de listar registros · dashboards mejor para datasets grandes.
8. Audit + compliance
Cada query queda en custom_sql_runs con texto · actor · timestamp · duración. Owner puede revisar trail en /custom-sql/audit · cumple ISO/SOC2 read-only access requirement. Logs retention según plan tenant.
Reference: apps/web/app/custom-sql/ · módulo tenant.custom_sql + validator @vertix/core/sql-validator.