Saltar a contenido

Motor SQL de Broadcast Lists

Además de las audiencias predefinidas, el constructor de audiencia por regla y la importación de CSV, Listas de difusión tiene una quinta opción para quien sepa SQL: el Gestor SQL (/broadcast-lists/gestor-sql), donde se escribe una consulta SELECT directa contra la base de datos para armar la lista.


Cómo funciona hoy (importante)

Aunque la consulta se escribe una sola vez, el resultado se guarda como una foto fija en el momento de importar — la lista queda como cualquier lista importada por CSV (segment = csv_import), no se vuelve a ejecutar sola. Si tu segmento tiene que reflejar cambios futuros en la base (por ejemplo, nuevos socios que cumplan la condición), tenés que volver a correr la consulta y reimportar.

Internamente existe un tipo de lista "dinámica" ligada a una consulta guardada (segment='sql_query', se re-ejecuta en cada envío), y el motor de ejecución ya la soporta — pero ninguna pantalla actual permite crearla todavía. Solo se llega al modo "foto fija" descrito arriba. Vale la pena tenerlo en cuenta si en algún momento se habilita ese flujo.

Cada consulta se puede guardar con nombre y descripción para reutilizarla, previsualizar (primeras 20 filas) antes de importar, y listar/borrar desde el mismo panel.


Reglas de seguridad

El motor (app/utils/sql_query_executor.py) valida la consulta antes de ejecutarla:

  • Tiene que ser una sola sentencia SELECT. Se bloquean comentarios (--, /*, $$) y cualquier palabra de escritura/administración (INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE, GRANT, EXECUTE, entre otras).
  • La conexión corre en modo solo lectura (SET TRANSACTION READ ONLY) mientras se ejecuta.
  • Si sos administrador, la consulta corre tal cual la escribiste, sin restricciones adicionales — podés ver datos de cualquier organización. Usalo con cuidado y agregá vos mismo el filtro de organización si no querés mezclar datos de distintos tenants.
  • Si NO sos administrador, pasan dos cosas:
    1. No podés referenciar directamente las tablas user, users, api_key, saved_query ni alembic_version.
    2. Tu consulta se envuelve automáticamente en un filtro que la limita a tu organización, cruzando el resultado por una columna llamada id contra los socios (asociates.id) de tu organización. Esto significa que tu SELECT tiene que devolver una columna llamada exactamente id (no un alias distinto) para que el filtro de organización pueda aplicarse correctamente.

No hay un límite de tiempo de ejecución configurado — una consulta pesada puede tardar, así que evitá SELECT * sobre tablas grandes sin filtrar.


Tablas más útiles para armar segmentos

Tabla Columnas relevantes
asociates id, organization_id, name, lastname, dni, email, phone
newsletter_subscriber id, organization_id, email, name, source, is_active
suscription id, associate_id, plan_id, state (0=pendiente, 1=fallida, 2=activa, 3=cancelada)
payment id, associate_id, recaudacion_id, amount, state (0/1/2=exitoso), payment_type (single_donation, collection_donation, subscription_payment)
plan id, organization_id, recaudacion_id, name, price
recaudacion id, organization_id, type, name, status, slug

No hay una relación (foreign key) directa entre asociates y newsletter_subscriber — si necesitás cruzarlas, hacelo por email (ver el ejemplo abajo). Tené en cuenta que ninguna de las dos columnas de email tiene una restricción de formato o mayúsculas/minúsculas, así que conviene normalizar con LOWER(TRIM(...)) al comparar.


Consultas de ejemplo

Socios que también están suscritos al newsletter

SELECT
    a.id,
    a.name,
    a.lastname,
    a.email,
    a.phone,
    n.source AS newsletter_source,
    n.created_at AS suscrito_desde
FROM asociates a
JOIN newsletter_subscriber n
    ON LOWER(TRIM(a.email)) = LOWER(TRIM(n.email))
WHERE n.is_active = true

(La columna id sin alias es la de asociates, necesaria para que el filtro automático de organización funcione si no sos administrador.)

Socios con una suscripción activa y su plan

SELECT
    a.id,
    a.name,
    a.lastname,
    a.email,
    a.phone,
    p.name AS plan,
    p.price AS monto_mensual
FROM asociates a
JOIN suscription s ON s.associate_id = a.id
JOIN plan p ON p.id = s.plan_id
WHERE s.state = 2

Donantes de una colecta específica

SELECT
    a.id,
    a.name,
    a.lastname,
    a.email,
    a.phone,
    pay.amount
FROM asociates a
JOIN payment pay ON pay.associate_id = a.id
JOIN recaudacion r ON r.id = pay.recaudacion_id
WHERE r.slug = 'nombre-de-la-colecta'
  AND pay.state = 2
  AND pay.payment_type = 'collection_donation'