Reten Docs
Filtros de eventos

Generar SQL

Compila un filtro de eventos a la cadena de CTEs que lo representa, desde el cuerpo o desde un filtro guardado.

Generar SQL

Compila una definición de filtro de eventos al SQL que la representa. Son dos rutas con la misma respuesta: una compila una definición enviada en el cuerpo y la otra un filtro ya guardado.

RutaOrigen de la definiciónCódigo de éxito
POST /api/event-criterias/generate-sqlEl cuerpo201
GET /api/event-criterias/:id/sqlEl filtro guardado200

Auth: Requerida — permiso event-criteria:view-sql en ambas rutas

El SQL no se ejecuta: estas rutas compilan y devuelven. Para validarlo contra BigQuery usa Validar; para ver filas reales, Previsualizar.


POST /api/event-criterias/generate-sql

Cuerpo de la solicitud

CampoTipoRequeridoDescripción
queryBlocksQueryBlockDto[]SíMínimo 1 elemento. La definición a compilar.
formatstringNoparameterized | literal. Default parameterized.

La estructura de cada bloque está en Definición.

Este endpoint recibe solo la definición: name y description no forman parte del cuerpo y enviarlos produce un 400 por propiedad desconocida.

Ejemplo

curl -X POST https://api.reten.ai/api/event-criterias/generate-sql \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>" \
  -H "x-project-id: <project-id>" \
  -H "Content-Type: application/json" \
  -d '{
    "queryBlocks": [
      {
        "order": 0,
        "from": { "table": "event_transactions", "joins": [] },
        "select": {
          "distinct": true,
          "items": [
            { "kind": "column", "column": { "table": "event_transactions", "column": "event_id" } }
          ]
        },
        "where": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "column": { "table": "event_transactions", "column": "status" },
              "comparisonOperator": "EQUALS",
              "value": "Facturada"
            },
            {
              "kind": "semiJoinPredicate",
              "table": "event_transactions",
              "semiJoin": "products",
              "subWhere": {
                "kind": "group",
                "logicalOperator": "AND",
                "children": [
                  {
                    "kind": "predicate",
                    "field": "categorization.category_name",
                    "comparisonOperator": "IN",
                    "value": ["Food Service", "HORECA"]
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }'
import axios from 'axios';

const body = {
  queryBlocks: [
    {
      order: 0,
      from: { table: 'event_transactions', joins: [] },
      select: {
        distinct: true,
        items: [
          { kind: 'column', column: { table: 'event_transactions', column: 'event_id' } },
        ],
      },
      where: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            column: { table: 'event_transactions', column: 'status' },
            comparisonOperator: 'EQUALS',
            value: 'Facturada',
          },
          {
            kind: 'semiJoinPredicate',
            table: 'event_transactions',
            semiJoin: 'products',
            subWhere: {
              kind: 'group',
              logicalOperator: 'AND',
              children: [
                {
                  kind: 'predicate',
                  field: 'categorization.category_name',
                  comparisonOperator: 'IN',
                  value: ['Food Service', 'HORECA'],
                },
              ],
            },
          },
        ],
      },
    },
  ],
};

const response = await axios.post(
  'https://api.reten.ai/api/event-criterias/generate-sql',
  body,
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const sql = response.data;

GET /api/event-criterias/:id/sql

Compila un filtro ya guardado. La definición se lee del filtro y se valida antes de compilarla.

Parámetros de ruta

ParámetroTipoDescripción
idUUIDID del filtro de eventos.

Parámetros de consulta

ParámetroTipoPor defectoDescripción
formatstringparameterizedparameterized | literal.

Ejemplo

curl "https://api.reten.ai/api/event-criterias/6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a/sql?format=literal" \
  -H "Authorization: Bearer <token>" \
  -H "x-tenant-id: <tenant-id>" \
  -H "x-project-id: <project-id>"
import axios from 'axios';

const id = '6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a';
const response = await axios.get(
  `https://api.reten.ai/api/event-criterias/${id}/sql`,
  {
    headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' },
    params: { format: 'literal' },
  },
);
const sql = response.data;

Formato de salida — format

El campo format decide cómo se emiten los valores del SQL y qué campos trae la respuesta. Es opcional; si se omite, el valor efectivo es parameterized.

formatCómo emite los valoresCampos extra en la respuesta
parameterizedPlaceholders con nombre @pN en el query; el binding va aparte.params, types
literalLos valores se inlinean como literales seguros dentro del query.(ninguno)
  • En generate-sql (POST) el format va en el cuerpo.
  • En :id/sql (GET) el format va como query param.

Solo se aceptan los strings "parameterized" y "literal"; cualquier otro valor produce un 400.

Respuesta — IEventCriteriaSql

Es una unión discriminada por format. Ambas variantes comparten una base y difieren en los campos de binding.

Base (común a los dos formatos):

CampoTipoDescripción
querystringUna cadena de CTEs (las cláusulas van separadas por saltos \n): sin el WITH inicial y sin el SELECT final, para insertarla dentro de un WITH … SELECT.
finalCteNamestringNombre del CTE final de la cadena; siempre CTE_VALID_EVENTS. Es el "handle" del resultado: un consumidor hace SELECT … FROM CTE_VALID_EVENTS.
subjectColumnstring | nullColumna de identidad que expone el CTE final. En un filtro de eventos es siempre null.
  • format: "parameterized":

    CampoTipoDescripción
    format"parameterized"Discriminador.
    paramsRecord<string, ScalarValue>Binding de cada @pN → su valor. ScalarValue = string | number | boolean.
    typesRecord<string, string>Tipo de parámetro de BigQuery de cada @pN: STRING, TIMESTAMP, BOOL, FLOAT64, INT64, NUMERIC, BIGNUMERIC.
  • format: "literal" — solo la base; sin params ni types.

En parameterized, params y types son parte del contrato: el query trae @pN y es inejecutable sin su binding y sus tipos. En literal no hay binding —los valores viven dentro del query— por eso no trae params ni types. Discrimina siempre por format antes de leer params/types.

subjectColumn es null en todo filtro de eventos: el CTE final proyecta eventos, no sujetos, y el catálogo no declara una columna de identidad. Las columnas que expone el CTE son las del select de la definición.

Ejemplo — parameterized (default)

Respuesta 201 al POST de ejemplo de arriba:

{
  "format": "parameterized",
  "query": "CTE_VALID_EVENTS AS (\nSELECT DISTINCT `event_transactions`.`event_id`\nFROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions\nWHERE `event_transactions`.`status` = @p0 AND EXISTS (SELECT 1 FROM UNNEST(`event_transactions`.`items`) AS `items_item`, `reten-prod.tenant_a1b2c3d4.product_properties` AS `product_properties` WHERE `product_properties`.`sku` = `items_item`.`item_id` AND (`product_properties`.`categorization`.`category_name` IN (@p1, @p2)))\n)",
  "finalCteName": "CTE_VALID_EVENTS",
  "subjectColumn": null,
  "params": { "p0": "Facturada", "p1": "Food Service", "p2": "HORECA" },
  "types": { "p0": "STRING", "p1": "STRING", "p2": "STRING" }
}

El query es una sola cadena con saltos \n. Formateado para lectura:

CTE_VALID_EVENTS AS (
SELECT DISTINCT `event_transactions`.`event_id`
FROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions
WHERE `event_transactions`.`status` = @p0
  AND EXISTS (
    SELECT 1
    FROM UNNEST(`event_transactions`.`items`) AS `items_item`,
         `reten-prod.tenant_a1b2c3d4.product_properties` AS `product_properties`
    WHERE `product_properties`.`sku` = `items_item`.`item_id`
      AND (`product_properties`.`categorization`.`category_name` IN (@p1, @p2))
  )
)

Cada @pN se liga con params[pN] (su valor) y types[pN] (su tipo BigQuery); sin ese binding el query no es ejecutable.

Cómo se consume la cadena

La respuesta no es un query ejecutable: es la cadena de CTEs que hay que envolver. En los snippets de abajo, {query} se sustituye por el valor del campo query tal cual viene: ya trae su propio CTE_VALID_EVENTS AS ( … ), así que el encabezado del CTE no se vuelve a escribir.

Para ejecutarla stand-alone:

WITH {query}
SELECT * FROM CTE_VALID_EVENTS

Para cruzarla contra otra consulta, se inserta {query} en el propio WITH del caller —seguido de una coma— y se hace JOIN CTE_VALID_EVENTS por la columna de evento que el select de la definición proyecta (por ejemplo event_id):

WITH
{query},
mis_metricas AS (
  SELECT t.event_id, t.pricing.final_amount
  FROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS t
)
SELECT SUM(m.final_amount) AS total
FROM mis_metricas AS m
JOIN CTE_VALID_EVENTS AS v ON v.event_id = m.event_id

El nombre CTE_VALID_EVENTS es estable y no depende del filtro: es el finalCteName de todo filtro de eventos. Las columnas que expone, en cambio, son las del select de cada definición.

Ejemplo — literal

La misma definición con format: "literal":

{
  "format": "literal",
  "query": "CTE_VALID_EVENTS AS (\nSELECT DISTINCT `event_transactions`.`event_id`\nFROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions\nWHERE `event_transactions`.`status` = 'Facturada' AND EXISTS (SELECT 1 FROM UNNEST(`event_transactions`.`items`) AS `items_item`, `reten-prod.tenant_a1b2c3d4.product_properties` AS `product_properties` WHERE `product_properties`.`sku` = `items_item`.`item_id` AND (`product_properties`.`categorization`.`category_name` IN ('Food Service', 'HORECA')))\n)",
  "finalCteName": "CTE_VALID_EVENTS",
  "subjectColumn": null
}

Los valores quedan inlineados como literales seguros (strings escapados y entre comillas simples, TIMESTAMP('…') para instantes, NUMERIC '…' para decimales exactos); no hay params ni types.

Reglas de validación

Antes de compilar, la definición pasa por las dos capas de validación, y ambas devuelven 400 Bad Request. La validación de negocio corre en ambos endpoints: generate-sql valida la definición del cuerpo, y :id/sql valida el filtro guardado antes de compilarlo.

El catálogo completo de reglas y sus mensajes exactos está en Definición → Reglas de validación.

Respuestas de error

EstadoDescripción
400DTO inválido (campo faltante, tipo incorrecto, enum fuera de rango, format no válido, propiedad desconocida como name/description) o una regla de negocio incumplida. En :id/sql, también si el id no es un UUID válido.
401Falta o es inválido el Authorization: Bearer.
403El usuario no tiene el permiso event-criteria:view-sql, o el x-tenant-id no está autorizado.
404En :id/sql, no existe un filtro de eventos con ese id en el tenant (Event criteria not found), o el tenant no tiene dataset configurado.