Generar SQL
Compila una definición de audience criteria a SQL de BigQuery, desde el cuerpo (aún sin persistir) o por id de un criterio guardado, en formato parametrizado o literal.
Generar SQL
Compila una definición de audience criteria a SQL de BigQuery (GoogleSQL). Un mismo motor sirve dos entradas:
POST /api/audience-criterias/generate-sql— compila una definición enviada en el cuerpo (aún no persistida).GET /api/audience-criterias/:id/sql— compila un criterio ya guardado, referenciado por suid.
Ambos devuelven la misma forma de respuesta: IAudienceCriteriaSql,
una unión discriminada por el campo format. El format (parametrizado o literal)
se elige por solicitud y gobierna tanto la forma del query como los campos de la
respuesta.
Dentro de un proyecto, el SQL de toda audiencia que no sea la audiencia raíz sale intersectado con la audiencia raíz del proyecto — ver Intersección con la audiencia raíz.
Auth: Requerida — permiso audience-criteria:view-sql (ambos endpoints).
El SQL generado está pensado para BigQuery: referencia las tablas como
`<gcp-project>.<dataset>.<tabla>`. El query es una cadena de CTEs intersectable —
sin el WITH inicial y sin el SELECT final— para que un consumidor la inserte
directo dentro de su propio WITH … SELECT. El último CTE se llama finalCteName
(CTE_AUDIENCE_USERS) y expone la columna de identidad subjectColumn (p. ej. user_id).
En el formato parametrizado usa parámetros con nombre @pN. El <gcp-project> (el proyecto
de Google Cloud, no un proyecto de Reten) sale de la configuración de ambiente de la API y el <dataset> se resuelve a partir del tenant; ninguno
forma parte del contrato de entrada.
POST /api/audience-criterias/generate-sql
Compila una definición enviada en el cuerpo, sin persistirla.
Auth: Requerida — permiso audience-criteria:view-sql
El endpoint responde 201 Created —no 200—, aunque no cree ningún recurso.
Cuerpo de la solicitud
Nivel raíz (GenerateSqlDto): la definición del criterio (CriteriaDefinitionDto)
más el campo format.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
queryBlocks | QueryBlockDto[] | Sí | Mínimo 1 elemento. Cada bloque ≈ un statement SQL completo. |
composedWith | CreateCompositionDto[] | No | Compone esta definición con otras audiencias vía operaciones de conjunto. |
format | "parameterized" | "literal" | No | Formato de salida. Default parameterized (ver Formato de salida). |
criteriaId | string (UUID) | null | No | La audiencia guardada que esta definición edita, para que el borrador dé el mismo SQL que esa audiencia dará al guardarse (ver Intersección con la audiencia raíz). Ausente o null: una audiencia nueva. Debe ser un UUID válido (si no, 400) de una audiencia del proyecto activo (si no, 404). |
A diferencia de Crear, la definición
no lleva name ni description: es solo el árbol de consulta. La validación es
estricta, así que enviar name/description (u otra propiedad desconocida) produce
un 400.
Definición del criterio
Es el mismo árbol que recibe Crear, sin los
campos name/description. Se documenta aquí de forma autocontenida.
queryBlocks[] — bloque de consulta
Cada elemento de queryBlocks (QueryBlockDto) es un statement SQL completo:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
order | number (entero) | Sí | Posición del bloque; entero ≥ 0. |
setOperator | SetOperatorEnum | No | Combina este bloque con el anterior. Ausente en el primer bloque; obligatorio en el resto (ver reglas). |
from | FromDto | Sí | Fuente de datos: tabla principal + joins. |
select | SelectDto | Sí | Proyección (columnas y/o agregaciones). |
timeWindow | TimeWindowDto | No | Ventana temporal del bloque (unión discriminada por timeOperator). |
where | WhereDto | Sí | Árbol de predicados. Obligatorio aunque sea un grupo vacío. |
groupBy | ColumnRefDto[] | No | Columnas de agrupación. |
having | HavingDto | No | Filtro sobre agregados. |
El setOperator de un query block combina bloques dentro de la misma definición;
no debe confundirse con el setOperator de una composición,
que combina criterios enteros. Son dos niveles distintos de operación de conjunto.
ColumnRefDto — referencia a columna
Se usa en todo el árbol (select, filtros, group by, joins, agregados, ventana temporal):
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Tabla del catálogo. No vacío. |
column | string | Sí | Columna. No vacío. Puede ser un path de STRUCT (p. ej. "pricing.final_amount"). |
Toda columna se cualifica siempre con su table para evitar ambigüedad.
from — fuente de datos (FromDto)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Tabla principal del catálogo. No vacío. |
joins | JoinDto[] | No | Cruces con otras tablas. Default []. |
unnests | ColumnRefDto[] | No | Columnas repetidas declaradas como fuente de filas (una fila por elemento). Una audiencia no las admite: omite el campo o envía [] (el default); una lista no vacía da 400. |
Cada JoinDto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | JoinTypeEnum | Sí | INNER | LEFT | RIGHT | FULL. |
table | string | Sí | Tabla a unir (del catálogo). No vacío. |
on | JoinPredicateDto[] | Sí | Condiciones de unión. |
Cada JoinPredicateDto es un par de columnas: { left: ColumnRefDto, right: ColumnRefDto }.
select — proyección (SelectDto)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
distinct | boolean | Sí | Aplica SELECT DISTINCT. |
items | (SelectColumnItem | SelectAggregateItem)[] | Sí | No vacío. Unión discriminada por el campo kind. |
Los items se discriminan por kind:
-
Columna —
kind: "column":Campo Tipo Requerido Descripción kind"column"Sí Discriminador. columnColumnRefDtoSí Columna proyectada. aliasstring No Debe matchear ^[A-Za-z_][A-Za-z0-9_]*$. -
Agregado —
kind: "aggregate":Campo Tipo Requerido Descripción kind"aggregate"Sí Discriminador. aggregateAggregateDtoSí Expresión de agregación. aliasstring Sí Aquí el alias es obligatorio. Mismo regex de identificador.
AggregateDto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
aggregationOperator | AggregationOperatorEnum | Sí | COUNT | SUM | AVG | MIN | MAX. |
operand | object | Sí | Lo que se agrega. Unión discriminada por kind: column, star o arithmetic (ver abajo). |
distinct | boolean | No | Aplica DISTINCT dentro del agregado. |
operand —lo que se agrega— se discrimina por kind:
kind | Campos adicionales | Significado |
|---|---|---|
column | column: ColumnRefDto | Una columna del catálogo. |
star | (ninguno) | *: cuenta filas. Solo es válido con COUNT y sin distinct (COUNT(DISTINCT *) no está permitido). |
arithmetic | arithmeticOperator: ArithmeticOperatorEnum, left, right | Expresión binaria. Cada lado es otro operando: una column o un arithmetic anidado (nunca star). |
ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (una división que da
NULL cuando el denominador es cero, en vez de hacer fallar la consulta). Las columnas de una
expresión aritmética tienen que ser numéricas (INT64, NUMERIC, BIGNUMERIC, FLOAT64), y las
expresiones se anidan hasta 3 niveles de arithmetic, contando el exterior.
where — árbol de predicados (WhereDto)
El where es un árbol recursivo. El nodo raíz es siempre un grupo, y sus hojas son de
dos tipos:
kind | Qué filtra |
|---|---|
group | No es una hoja: agrupa hijos con AND / OR. Se pueden anidar grupos. |
predicate | Una columna escalar de una tabla del from. |
recordPredicate | Una columna RECORD REPEATED: existe ≥1 elemento que cumple el sub-filtro. |
Grupo — kind: "group"
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
kind | "group" | Sí | Discriminador. |
logicalOperator | LogicalOperatorEnum (AND | OR) | Sí | Conectivo entre los hijos. |
children | (Grupo | Hoja)[] | Sí | Hijos del grupo; pueden ser grupos anidados. |
logicalOperator es obligatorio siempre en un grupo, pero solo tiene efecto con 2 o
más hijos. Con menos de 2 hijos su valor es indiferente (genera el mismo SQL), pero igual
debe enviarse. Un where sin filtros se expresa como un grupo vacío:
{ "kind": "group", "logicalOperator": "AND", "children": [] }.
Hoja de columna — kind: "predicate"
Compara una columna escalar cualificada por su tabla.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
kind | "predicate" | Sí | Discriminador. |
column | ColumnRefDto | Sí | Columna del predicado. |
comparisonOperator | ComparisonOperatorEnum | Sí | Operador de comparación. |
value | PredicateValue | Depende | Su forma depende del operador. |
{
"kind": "predicate",
"column": { "table": "user_properties", "column": "country" },
"comparisonOperator": "IN",
"value": ["CL", "MX"]
}Hoja de registro — kind: "recordPredicate"
Filtra por una columna RECORD REPEATED (un array de structs, como attributes).
Significa existe al menos un elemento del array que cumple el sub-filtro.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
kind | "recordPredicate" | Sí | Discriminador. |
column | ColumnRefDto | Sí | Columna RECORD REPEATED de una tabla del from. |
subWhere | Sub-árbol relativo | Sí | Condiciones sobre los sub-campos del elemento. No puede quedar vacío. |
{
"kind": "recordPredicate",
"column": { "table": "event_transactions", "column": "attributes" },
"subWhere": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{ "kind": "predicate", "field": "key", "comparisonOperator": "EQUALS", "value": "canal" },
{ "kind": "predicate", "field": "value", "comparisonOperator": "EQUALS", "value": "web" }
]
}
}Las condiciones del subWhere se evalúan sobre un mismo elemento del array, no sobre
el array entero. El ejemplo de arriba selecciona filas que tienen un atributo cuya key
es canal y cuyo value es web — no filas que tengan un atributo con esa key y
otro, distinto, con ese value.
Qué columnas admiten un filtro de registro lo dice el
catálogo: las que vienen con
kind: "record".
Sub-árbol relativo
El subWhere es un grupo AND/OR cuyas hojas se resuelven contra los sub-campos que el
registro declara.
La diferencia con el árbol de primer nivel es una sola, y es la fuente de error más común:
Dentro de un sub-árbol relativo los operandos no llevan tabla: se escribe
"field": "key", no "column": { "table": "attributes", "column": "key" }. La hoja usa
la clave field (un string), no column.
Hojas admitidas dentro de un sub-árbol relativo:
- Hoja escalar —
{ "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. Elfielddebe ser un sub-campo escalar del registro. - Registro anidado —
{ "kind": "recordPredicate", "field": string, "subWhere": … }. Un registro dentro de otro registro; se anida sin límite declarado, siempre que el catálogo declare cada nivel. - Grupo — la misma forma de grupo, para combinar las hojas anteriores con
AND/OR.
Forma de value según comparisonOperator
| Operadores | Forma de value | Ejemplo |
|---|---|---|
IS_NULL, IS_NOT_NULL | Sin value (se omite) | (se omite) |
IN, NOT_IN | Lista no vacía ScalarValue[] | ["CL", "MX"] |
BETWEEN | Par [min, max] | [10, 20] |
El resto (EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH) | Escalar único | "activo" · 42 |
ScalarValue = string | number | boolean. Además, cada operador debe ser
compatible con el tipo de la columna (ver reglas de validación).
groupBy — columnas de agrupación
Lista opcional de ColumnRefDto[]. Cuando el bloque agrega (declara groupBy o tiene un
agregado en el select), el groupBy debe coincidir exactamente con las columnas
no agregadas del select (ver reglas de validación).
having — filtro sobre agregados (HavingDto)
Misma estructura recursiva que where (grupos AND/OR anidados y misma regla de value
por operador), pero la hoja opera sobre un agregado, no sobre una columna:
- Predicado (hoja) —
{ kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }. - Grupo — idéntico al de
where.
timeWindow — ventana temporal (TimeWindowDto)
Unión discriminada por timeOperator. Acota el bloque a un período de tiempo sobre una
columna TIMESTAMP del catálogo (reference). Es un campo hermano del where, no un
nodo del árbol de filtros.
timeOperator | Campos adicionales |
|---|---|
ALL_TIME | (ninguno) — sin acotar por tiempo. |
IN_THE_LAST | reference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0 |
IN_THE_FIRST | reference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0 |
SINCE | reference: ColumnRefDto, from: string (YYYY-MM-DD) |
BETWEEN | reference: ColumnRefDto, from: string (YYYY-MM-DD), to: string (YYYY-MM-DD) |
TimeUnitEnum: DAYS | WEEKS | MONTHS | YEARS.
Semántica de negocio de cada operador:
ALL_TIME— sin ventana; considera todo el histórico.IN_THE_LAST— ventana rodante relativa a "ahora": los últimosvalueunit(p. ej. últimos30 DAYS). Se recomputa en cada corrida.IN_THE_FIRST— ventana relativa al origen del ciclo de vida del usuario (su fecha de alta): los primerosvalueunitde vida de cada usuario. Se ancla por usuario, no a una fecha fija.SINCE— desde la fecha civilfrom(inclusiva) en adelante.BETWEEN— entrefromyto, ambas fechas civiles inclusivas de su día completo.
Las fechas civiles YYYY-MM-DD (SINCE, BETWEEN) se resuelven al inicio del día en
la zona horaria del tenant. El detalle de zona horaria es interno; basta con enviar la
fecha civil.
composedWith[] — composición con otras audiencias
Cada elemento (CreateCompositionDto) combina esta definición con otra audiencia
vía una operación de conjunto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
operandCriteriaId | string (UUID) | Sí | Id de la otra audiencia (operando). Debe existir. |
setOperator | SetOperatorEnum | Sí | UNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT. |
Ejemplo
Definición de una audiencia de usuarios con una transacción dentro de sus primeros 30 días
desde su fecha de alta (IN_THE_FIRST). Al omitir format, la respuesta es parametrizada (default).
curl -X POST https://api.reten.ai/api/audience-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" },
"select": {
"distinct": true,
"items": [
{
"kind": "column",
"column": { "table": "event_transactions", "column": "user_id" }
}
]
},
"timeWindow": {
"timeOperator": "IN_THE_FIRST",
"reference": { "table": "event_transactions", "column": "event_date" },
"unit": "DAYS",
"value": 30
},
"where": { "kind": "group", "logicalOperator": "AND", "children": [] }
}
]
}'import axios from 'axios';
const body = {
queryBlocks: [
{
order: 0,
from: { table: 'event_transactions' },
select: {
distinct: true,
items: [
{
kind: 'column',
column: { table: 'event_transactions', column: 'user_id' },
},
],
},
timeWindow: {
timeOperator: 'IN_THE_FIRST',
reference: { table: 'event_transactions', column: 'event_date' },
unit: 'DAYS',
value: 30,
},
where: { kind: 'group', logicalOperator: 'AND', children: [] },
},
],
// format: 'literal', // opcional; por defecto 'parameterized'
};
const response = await axios.post(
'https://api.reten.ai/api/audience-criterias/generate-sql',
body,
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const sql = response.data; // IAudienceCriteriaSqlLa respuesta se documenta en IAudienceCriteriaSql, con
un ejemplo de cada formato.
GET /api/audience-criterias/:id/sql
Compila un criterio ya guardado, referenciado por su id. La definición no viaja en
el cuerpo: se toma del criterio persistido en el proyecto activo.
Auth: Requerida — permiso audience-criteria:view-sql
Estado 200 OK (es un GET).
Parámetros de ruta
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
id | UUID | Sí | Id del audience criteria. Debe ser un UUID válido (si no, 400). |
Parámetros de query
GetSqlDto:
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
format | "parameterized" | "literal" | No | Formato de salida. Default parameterized (ver Formato de salida). |
Ejemplo
curl "https://api.reten.ai/api/audience-criterias/f47ac10b-58cc-4372-a567-0e02b2c3d479/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 = 'f47ac10b-58cc-4372-a567-0e02b2c3d479';
const response = await axios.get(
`https://api.reten.ai/api/audience-criterias/${id}/sql`,
{
params: { format: 'literal' }, // opcional; por defecto 'parameterized'
headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' },
},
);
const sql = response.data; // IAudienceCriteriaSqlFormato 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.
format | Cómo emite los valores | Campos extra en la respuesta |
|---|---|---|
parameterized | Placeholders con nombre @pN en el query; el binding va aparte. | params, types |
literal | Los valores se inlinean como literales seguros dentro del query. | (ninguno) |
- En
generate-sql(POST) elformatva en el cuerpo. - En
:id/sql(GET) elformatva como query param.
Solo se aceptan los strings "parameterized" y "literal"; cualquier otro valor produce
un 400.
Respuesta — IAudienceCriteriaSql
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):
| Campo | Tipo | Descripción |
|---|---|---|
query | string | Una cadena de CTEs intersectable (una sola cadena; las cláusulas van separadas por saltos \n): sin el WITH inicial y sin el SELECT final, para insertarla dentro de un WITH … SELECT. |
finalCteName | string | Nombre del CTE final de la cadena (estable: CTE_AUDIENCE_USERS); es el "handle" del resultado de la audiencia — un consumidor hace JOIN {finalCteName} / SELECT … FROM {finalCteName}. |
subjectColumn | string | Columna de identidad que expone el CTE final (p. ej. user_id). |
granularity | "table_row" | "record_element" | Qué es una fila del CTE final. En una audiencia es siempre table_row: una fila por fila de la tabla, una por usuario. El otro valor del contrato, record_element (una fila por elemento de un registro repetido declarado como fuente de filas), no se da en audiencias, que no admiten fuentes de filas. |
-
format: "parameterized"—IParameterizedAudienceCriteriaSql:Campo Tipo Descripción format"parameterized"Discriminador. querystring Cadena de CTEs con placeholders @pN.finalCteNamestring Nombre del CTE final. subjectColumnstring Columna de identidad que expone el CTE final. granularity"table_row"|"record_element"Qué es una fila del CTE final (en una audiencia, siempre table_row).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(p. ej.STRING,INT64,TIMESTAMP,BOOL,FLOAT64,NUMERIC,BIGNUMERIC). -
format: "literal"—ILiteralAudienceCriteriaSql:Campo Tipo Descripción format"literal"Discriminador. querystring Cadena de CTEs con los valores ya inlineados como literales. finalCteNamestring Nombre del CTE final. subjectColumnstring Columna de identidad que expone el CTE final. granularity"table_row"|"record_element"Qué es una fila del CTE final (en una audiencia, siempre table_row).
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.
Ejemplo — parameterized (default)
Respuesta 201 al POST de ejemplo de arriba (sin format):
{
"format": "parameterized",
"query": "op_5e2a91c07b3d_0 AS (\nSELECT DISTINCT `event_transactions`.`user_id`\nFROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions\nINNER JOIN `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties ON `event_transactions`.`user_id` = `user_properties`.`user_id`\nWHERE `event_transactions`.`event_date` BETWEEN `user_properties`.`sign_up_date` AND TIMESTAMP(DATETIME_ADD(DATETIME(`user_properties`.`sign_up_date`), INTERVAL @p0 DAY))\n),\nop_9a8b7c6d5e4f_0 AS (\nSELECT DISTINCT `user_properties`.`user_id`\nFROM `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties\nWHERE `user_properties`.`is_active` = @p1\n),\nCTE_AUDIENCE_USERS AS (\nSELECT * FROM op_5e2a91c07b3d_0\nINTERSECT DISTINCT\nSELECT * FROM op_9a8b7c6d5e4f_0\n)",
"finalCteName": "CTE_AUDIENCE_USERS",
"subjectColumn": "user_id",
"granularity": "table_row",
"params": { "p0": 30, "p1": true },
"types": { "p0": "INT64", "p1": "BOOL" }
}El query es una sola cadena con saltos \n — una cadena de CTEs sin el WITH inicial
y sin el SELECT final. Formateado para lectura:
op_5e2a91c07b3d_0 AS (
SELECT DISTINCT `event_transactions`.`user_id`
FROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions
INNER JOIN `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties ON `event_transactions`.`user_id` = `user_properties`.`user_id`
WHERE `event_transactions`.`event_date` BETWEEN `user_properties`.`sign_up_date` AND TIMESTAMP(DATETIME_ADD(DATETIME(`user_properties`.`sign_up_date`), INTERVAL @p0 DAY))
),
op_9a8b7c6d5e4f_0 AS (
SELECT DISTINCT `user_properties`.`user_id`
FROM `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties
WHERE `user_properties`.`is_active` = @p1
),
CTE_AUDIENCE_USERS AS (
SELECT * FROM op_5e2a91c07b3d_0
INTERSECT DISTINCT
SELECT * FROM op_9a8b7c6d5e4f_0
)Cada @pN se liga con params[pN] (su valor) y types[pN] (su tipo BigQuery); sin ese
binding el query no es ejecutable. p0 es de la audiencia y p1 de la audiencia raíz
del proyecto, que aquí conserva su definición por defecto (ver
Intersección con la audiencia raíz). El INNER JOIN a
user_properties no está en la solicitud: IN_THE_FIRST mide los primeros días de cada usuario
desde su fecha de alta, así que la API lo agrega.
Para ejecutarla stand-alone, envuelve la cadena con el nombre del CTE de la respuesta:
WITH {query} SELECT * FROM {finalCteName}. Para intersectarla contra otra query,
inserta {query} en tu propio WITH y haz JOIN {finalCteName} por {subjectColumn}.
Ejemplo — literal
La misma definición con format: "literal" (en la variante by-id, ?format=literal):
{
"format": "literal",
"query": "op_5e2a91c07b3d_0 AS (\nSELECT DISTINCT `event_transactions`.`user_id`\nFROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions\nINNER JOIN `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties ON `event_transactions`.`user_id` = `user_properties`.`user_id`\nWHERE `event_transactions`.`event_date` BETWEEN `user_properties`.`sign_up_date` AND TIMESTAMP(DATETIME_ADD(DATETIME(`user_properties`.`sign_up_date`), INTERVAL 30 DAY))\n),\nop_9a8b7c6d5e4f_0 AS (\nSELECT DISTINCT `user_properties`.`user_id`\nFROM `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties\nWHERE `user_properties`.`is_active` = TRUE\n),\nCTE_AUDIENCE_USERS AS (\nSELECT * FROM op_5e2a91c07b3d_0\nINTERSECT DISTINCT\nSELECT * FROM op_9a8b7c6d5e4f_0\n)",
"finalCteName": "CTE_AUDIENCE_USERS",
"subjectColumn": "user_id",
"granularity": "table_row"
}Formateado para lectura:
op_5e2a91c07b3d_0 AS (
SELECT DISTINCT `event_transactions`.`user_id`
FROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions
INNER JOIN `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties ON `event_transactions`.`user_id` = `user_properties`.`user_id`
WHERE `event_transactions`.`event_date` BETWEEN `user_properties`.`sign_up_date` AND TIMESTAMP(DATETIME_ADD(DATETIME(`user_properties`.`sign_up_date`), INTERVAL 30 DAY))
),
op_9a8b7c6d5e4f_0 AS (
SELECT DISTINCT `user_properties`.`user_id`
FROM `reten-prod.tenant_a1b2c3d4.user_properties` AS user_properties
WHERE `user_properties`.`is_active` = TRUE
),
CTE_AUDIENCE_USERS AS (
SELECT * FROM op_5e2a91c07b3d_0
INTERSECT DISTINCT
SELECT * FROM op_9a8b7c6d5e4f_0
)Los valores quedan inlineados como literales seguros (aquí 30 y TRUE; los strings van
escapados y entre comillas simples); no hay params ni types.
Intersección con la audiencia raíz
Todo proyecto tiene una audiencia raíz, y toda otra audiencia del proyecto se evalúa dentro de ella. En ambos endpoints, el SQL de cualquier audiencia que no sea la audiencia raíz sale intersectado con la audiencia raíz del proyecto:
- La audiencia y la audiencia raíz conservan sus propios CTEs, y el CTE final intersecta el último CTE de cada una:
CTE_AUDIENCE_USERS AS (SELECT * FROM <último CTE de la audiencia> INTERSECT DISTINCT SELECT * FROM <último CTE de la audiencia raíz>). - En el formato
parameterized, los parámetros@pNde la audiencia van primero y los de la audiencia raíz a continuación. - No hay intersección cuando la audiencia compilada es la propia audiencia raíz —
GET /:id/sqlcon elidde la audiencia raíz, o un borrador cuyocriteriaIdes la audiencia raíz. - La intersección es posicional (
SELECT *en los dos lados): la audiencia debe proyectar las mismas columnas que la audiencia raíz, en el mismo orden y con tipos compatibles — con la definición por defecto de la audiencia raíz, solouser_id. Si no, el SQL falla al ejecutarse.
Los CTEs intermedios llevan en su nombre un prefijo del id de cada audiencia (en un borrador
enviado sin criteriaId, ese prefijo cambia en cada solicitud), así que referencia siempre el
resultado a través de finalCteName.
Por ejemplo, una audiencia de usuarios con una transacción COMPLETED, en un proyecto cuya
audiencia raíz se editó a country = 'CL' (formato literal, formateado para lectura):
op_f47ac10b58cc_0 AS (
SELECT DISTINCT `event_transactions`.`user_id`
FROM `<gcp-project>.<dataset>.event_transactions` AS event_transactions
WHERE `event_transactions`.`status` = 'COMPLETED'
),
op_9a8b7c6d5e4f_0 AS (
SELECT DISTINCT `user_properties`.`user_id`
FROM `<gcp-project>.<dataset>.user_properties` AS user_properties
WHERE `user_properties`.`country` = 'CL'
),
CTE_AUDIENCE_USERS AS (
SELECT * FROM op_f47ac10b58cc_0
INTERSECT DISTINCT
SELECT * FROM op_9a8b7c6d5e4f_0
)Reglas de validación
Antes de compilar, la definición pasa por las mismas dos capas de validación que
Crear, y ambas
devuelven 400 Bad Request:
- Validación del DTO (estructura, tipos, enums, campos requeridos,
formatválido).messagees un arreglo de strings y se rechazan propiedades desconocidas (incluidosname/description). - Validación de negocio (la definición debe generar SQL válido y coherente contra el
catálogo).
messagees un string único con la regla incumplida.
La validación de negocio corre en ambos endpoints: generate-sql valida la
definición del cuerpo, y :id/sql valida el criterio guardado antes de compilarlo. El
catálogo completo de reglas y sus mensajes exactos está en
Crear → Reglas de validación.
Respuestas de error
| Estado | Descripción |
|---|---|
400 | DTO 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; en generate-sql, si no lo es el criteriaId. |
401 | Falta o es inválido el Authorization: Bearer. |
403 | El usuario no tiene el permiso audience-criteria:view-sql, o el x-tenant-id no está autorizado. |
404 | En :id/sql, no existe un audience criteria con ese id en el proyecto activo. En generate-sql, un operandCriteriaId de composedWith o el criteriaId no existe en el proyecto activo. |
Eliminar Audiencia
Archiva (soft delete) un audience criteria; responde 204 sin cuerpo. Falla con 409 si es la audiencia raíz del proyecto, y con 400 si otro criterio lo referencia como operando.
Validar
Valida una definición de audience criteria y ejecuta un dry-run contra BigQuery, desde el cuerpo (aún sin persistir) o por id de un criterio guardado; devuelve si es válida y el costo estimado, sin datos ni SQL.