Reten Docs
Audiências

Gerar SQL

Compila uma definição de audience criteria para SQL do BigQuery, a partir do corpo da requisição (ainda não persistida) ou por id de um critério salvo, em formato parametrizado ou literal.

Gerar SQL

Compila uma definição de audience criteria para SQL do BigQuery (GoogleSQL). Um mesmo motor atende duas entradas:

  • POST /api/audience-criterias/generate-sql — compila uma definição enviada no corpo (ainda não persistida).
  • GET /api/audience-criterias/:id/sql — compila um critério já salvo, referenciado por seu id.

Ambos retornam a mesma forma de resposta: IAudienceCriteriaSql, uma união discriminada pelo campo format. O format (parametrizado ou literal) é escolhido por requisição e governa tanto a forma do query quanto os campos da resposta.

Dentro de um projeto, o SQL de toda audiência que não seja a audiência raiz sai intersectado com a audiência raiz do projeto — veja Interseção com a audiência raiz.

Auth: Obrigatória — permissão audience-criteria:view-sql (ambos endpoints).

O SQL gerado é pensado para o BigQuery: referencia as tabelas como `<gcp-project>.<dataset>.<table>`. O query é uma cadeia de CTEs intersectável — sem o WITH inicial e sem o SELECT final — para que um consumidor a insira direto dentro de seu próprio WITH … SELECT. O último CTE se chama finalCteName (CTE_AUDIENCE_USERS) e expõe a coluna de identidade subjectColumn (ex.: user_id). No formato parametrizado usa parâmetros nomeados @pN. O <gcp-project> (o projeto do Google Cloud, não um projeto da Reten) vem da configuração de ambiente da API e o <dataset> é resolvido a partir do tenant; nenhum dos dois faz parte do contrato de entrada.


POST /api/audience-criterias/generate-sql

Compila uma definição enviada no corpo, sem persisti-la.

Auth: Obrigatória — permissão audience-criteria:view-sql

O endpoint responde 201 Created —não 200—, mesmo sem criar nenhum recurso.

Corpo da requisição

Nível raiz (GenerateSqlDto): a definição do critério (CriteriaDefinitionDto) mais o campo format.

CampoTipoObrigatórioDescrição
queryBlocksQueryBlockDto[]SimMínimo 1 elemento. Cada bloco ≈ um statement SQL completo.
composedWithCreateCompositionDto[]NãoCompõe esta definição com outras audiências via operações de conjunto.
format"parameterized" | "literal"NãoFormato de saída. Padrão parameterized (ver Formato de saída).
criteriaIdstring (UUID) | nullNãoA audiência salva que esta definição edita, para que o rascunho gere o mesmo SQL que essa audiência gerará ao ser salva (veja Interseção com a audiência raiz). Ausente ou null: uma audiência nova. Deve ser um UUID válido (caso contrário, 400) de uma audiência do projeto ativo (caso contrário, 404).

Diferentemente de Criar, a definição não leva name nem description: é apenas a árvore de consulta. A validação é estrita, então enviar name/description (ou qualquer outra propriedade desconhecida) produz um 400.

Definição do critério

É a mesma árvore que Criar recebe, sem os campos name/description. Está documentada aqui de forma autocontida.

queryBlocks[] — bloco de consulta

Cada elemento de queryBlocks (QueryBlockDto) é um statement SQL completo:

CampoTipoObrigatórioDescrição
ordernumber (inteiro)SimPosição do bloco; inteiro ≥ 0.
setOperatorSetOperatorEnumNãoCombina este bloco com o anterior. Ausente no primeiro bloco; obrigatório nos demais (ver regras).
fromFromDtoSimFonte de dados: tabela principal + joins.
selectSelectDtoSimProjeção (colunas e/ou agregações).
timeWindowTimeWindowDtoNãoJanela temporal do bloco (união discriminada por timeOperator).
whereWhereDtoSimÁrvore de predicados. Obrigatório mesmo que seja um grupo vazio.
groupByColumnRefDto[]NãoColunas de agrupamento.
havingHavingDtoNãoFiltro sobre agregados.

O setOperator de um query block combina blocos dentro da mesma definição; não deve ser confundido com o setOperator de uma composição, que combina critérios inteiros. São dois níveis distintos de operação de conjunto.

ColumnRefDto — referência a coluna

Usado em toda a árvore (select, filtros, group by, joins, agregados, janela temporal):

CampoTipoObrigatórioDescrição
tablestringSimTabela do catálogo. Não vazio.
columnstringSimColuna. Não vazio. Pode ser um path de STRUCT (ex.: "pricing.final_amount").

Toda coluna é sempre qualificada com sua table para evitar ambiguidade.

from — fonte de dados (FromDto)
CampoTipoObrigatórioDescrição
tablestringSimTabela principal do catálogo. Não vazio.
joinsJoinDto[]NãoJoins com outras tabelas. Padrão [].
unnestsColumnRefDto[]NãoColunas repetidas declaradas como fonte de linhas (uma linha por elemento). Uma audiência não as aceita: omita o campo ou envie [] (o padrão); uma lista não vazia retorna 400.

Cada JoinDto:

CampoTipoObrigatórioDescrição
typeJoinTypeEnumSimINNER | LEFT | RIGHT | FULL.
tablestringSimTabela a unir (do catálogo). Não vazio.
onJoinPredicateDto[]SimCondições de junção.

Cada JoinPredicateDto é um par de colunas: { left: ColumnRefDto, right: ColumnRefDto }.

select — projeção (SelectDto)
CampoTipoObrigatórioDescrição
distinctbooleanSimAplica SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]SimNão vazio. União discriminada pelo campo kind.

Os items são discriminados por kind:

  • Coluna — kind: "column":

    CampoTipoObrigatórioDescrição
    kind"column"SimDiscriminador.
    columnColumnRefDtoSimColuna projetada.
    aliasstringNãoDeve casar com ^[A-Za-z_][A-Za-z0-9_]*$.
  • Agregado — kind: "aggregate":

    CampoTipoObrigatórioDescrição
    kind"aggregate"SimDiscriminador.
    aggregateAggregateDtoSimExpressão de agregação.
    aliasstringSimAqui o alias é obrigatório. Mesmo regex de identificador.

AggregateDto:

CampoTipoObrigatórioDescrição
aggregationOperatorAggregationOperatorEnumSimCOUNT | SUM | AVG | MIN | MAX.
operandobjectSimO que é agregado. União discriminada por kind: column, star ou arithmetic (veja abaixo).
distinctbooleanNãoAplica DISTINCT dentro do agregado.

operand —o que é agregado— é discriminado por kind:

kindCampos adicionaisSignificado
columncolumn: ColumnRefDtoUma coluna do catálogo.
star(nenhum)*: conta linhas. Só é válido com COUNT e sem distinct (COUNT(DISTINCT *) não é permitido).
arithmeticarithmeticOperator: ArithmeticOperatorEnum, left, rightExpressão binária. Cada lado é outro operando: uma column ou um arithmetic aninhado (nunca star).

ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (uma divisão que retorna NULL quando o denominador é zero, em vez de fazer a consulta falhar). As colunas de uma expressão aritmética precisam ser numéricas (INT64, NUMERIC, BIGNUMERIC, FLOAT64), e as expressões se aninham até 3 níveis de arithmetic, contando o externo.

where — árvore de predicados (WhereDto)

O where é uma árvore recursiva. O nó raiz é sempre um grupo, e suas folhas são de dois tipos:

kindO que filtra
groupNão é uma folha: agrupa filhos com AND / OR. Os grupos podem ser aninhados.
predicateUma coluna escalar de uma tabela do from.
recordPredicateUma coluna RECORD REPEATED: pelo menos 1 elemento satisfaz o sub-filtro.
Grupo — kind: "group"
CampoTipoObrigatórioDescrição
kind"group"SimDiscriminador.
logicalOperatorLogicalOperatorEnum (AND | OR)SimConectivo entre os filhos.
children(Group | Leaf)[]SimFilhos do grupo; podem ser grupos aninhados.

logicalOperator é sempre obrigatório em um grupo, mas só tem efeito com 2 ou mais filhos. Com menos de 2 filhos seu valor é irrelevante (gera o mesmo SQL), mas ainda assim deve ser enviado. Um where sem filtros é expresso como um grupo vazio: { "kind": "group", "logicalOperator": "AND", "children": [] }.

Folha de coluna — kind: "predicate"

Compara uma coluna escalar qualificada por sua tabela.

CampoTipoObrigatórioDescrição
kind"predicate"SimDiscriminador.
columnColumnRefDtoSimColuna do predicado.
comparisonOperatorComparisonOperatorEnumSimOperador de comparação.
valuePredicateValueDependeSeu formato depende do operador.
{
  "kind": "predicate",
  "column": { "table": "user_properties", "column": "country" },
  "comparisonOperator": "IN",
  "value": ["CL", "MX"]
}
Folha de registro — kind: "recordPredicate"

Filtra por uma coluna RECORD REPEATED (um array de structs, como attributes). Significa que pelo menos um elemento do array satisfaz o sub-filtro.

CampoTipoObrigatórioDescrição
kind"recordPredicate"SimDiscriminador.
columnColumnRefDtoSimColuna RECORD REPEATED de uma tabela do from.
subWhereSub-árvore relativaSimCondições sobre os sub-campos do elemento. Não pode ficar vazio.
{
  "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" }
    ]
  }
}

As condições do subWhere são avaliadas sobre um mesmo elemento do array, e não sobre o array inteiro. O exemplo acima seleciona linhas que têm um atributo cujo key é canal e cujo value é web — não linhas que têm um atributo com esse key e outro, diferente, com esse value.

Quais colunas aceitam um filtro de registro é informado pelo catálogo: as que vêm com kind: "record".

Sub-árvore relativa

O subWhere é um grupo AND/OR cujas folhas são resolvidas contra os sub-campos que o registro declara.

Há uma única diferença em relação à árvore de nível superior, e ela é a origem mais comum de erros:

Dentro de uma sub-árvore relativa os operandos não levam tabela: você escreve "field": "key", e não "column": { "table": "attributes", "column": "key" }. A folha usa a chave field (uma string), não column.

Folhas permitidas dentro de uma sub-árvore relativa:

  • Folha escalar — { "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. O field deve ser um sub-campo escalar do registro.
  • Registro aninhado — { "kind": "recordPredicate", "field": string, "subWhere": … }. Um registro dentro de outro registro; aninha sem limite declarado, desde que o catálogo declare cada nível.
  • Grupo — o mesmo formato de grupo, para combinar as folhas anteriores com AND/OR.
Forma de value por comparisonOperator
OperadoresForma de valueExemplo
IS_NULL, IS_NOT_NULLSem value (omitido)(omitido)
IN, NOT_INLista não vazia ScalarValue[]["CL", "MX"]
BETWEENPar [min, max][10, 20]
O 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"active" · 42

ScalarValue = string | number | boolean. Além disso, cada operador deve ser compatível com o tipo da coluna (ver regras de validação).

groupBy — colunas de agrupamento

Lista opcional de ColumnRefDto[]. Quando o bloco agrega (declara groupBy ou tem um agregado no select), o groupBy deve coincidir exatamente com as colunas não agregadas do select (ver regras de validação).

having — filtro sobre agregados (HavingDto)

Mesma estrutura recursiva que where (grupos AND/OR aninhados e mesma regra de value por operador), mas a folha opera sobre um agregado, não sobre uma coluna:

  • Predicado (folha) — { kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }.
  • Grupo — idêntico ao de where.
timeWindow — janela temporal (TimeWindowDto)

União discriminada por timeOperator. Restringe o bloco a um período de tempo sobre uma coluna TIMESTAMP do catálogo (reference). É um campo irmão de where, não um nó da árvore de filtros.

timeOperatorCampos adicionais
ALL_TIME(nenhum) — sem restrição de tempo.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: inteiro > 0
IN_THE_FIRSTreference: ColumnRefDto, unit: TimeUnitEnum, value: inteiro > 0
SINCEreference: ColumnRefDto, from: string (YYYY-MM-DD)
BETWEENreference: ColumnRefDto, from: string (YYYY-MM-DD), to: string (YYYY-MM-DD)

TimeUnitEnum: DAYS | WEEKS | MONTHS | YEARS.

Semântica de negócio de cada operador:

  • ALL_TIME — sem janela; considera todo o histórico.
  • IN_THE_LAST — janela móvel relativa a "agora": os últimos value unit (ex.: últimos 30 DAYS). Recalculada a cada execução.
  • IN_THE_FIRST — janela relativa à origem do ciclo de vida do usuário (sua data de cadastro): os primeiros value unit de vida de cada usuário. Ancorada por usuário, não a uma data fixa.
  • SINCE — a partir da data civil from (inclusive).
  • BETWEEN — entre from e to, ambas datas civis inclusivas do dia completo.

As datas civis YYYY-MM-DD (SINCE, BETWEEN) são resolvidas para o início do dia no fuso horário do tenant. O detalhe do fuso horário é interno; basta enviar a data civil.

composedWith[] — composição com outras audiências

Cada elemento (CreateCompositionDto) combina esta definição com outra audiência via uma operação de conjunto:

CampoTipoObrigatórioDescrição
operandCriteriaIdstring (UUID)SimId da outra audiência (operando). Deve existir.
setOperatorSetOperatorEnumSimUNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT.

Exemplo

Definição de uma audiência de usuários com uma transação dentro dos seus primeiros 30 dias desde a data de cadastro (IN_THE_FIRST). Ao omitir format, a resposta é parametrizada (padrão).

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; padrão é '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; // IAudienceCriteriaSql

A resposta é documentada em IAudienceCriteriaSql, com um exemplo de cada formato.


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

Compila um critério já salvo, referenciado por seu id. A definição não viaja no corpo: é obtida do critério persistido no projeto ativo.

Auth: Obrigatória — permissão audience-criteria:view-sql

Status 200 OK (é um GET).

Parâmetros de rota

ParâmetroTipoObrigatórioDescrição
idUUIDSimId do audience criteria. Deve ser um UUID válido (caso contrário, 400).

Parâmetros de consulta

GetSqlDto:

ParâmetroTipoObrigatórioDescrição
format"parameterized" | "literal"NãoFormato de saída. Padrão parameterized (ver Formato de saída).

Exemplo

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; padrão é 'parameterized'
    headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' },
  },
);
const sql = response.data; // IAudienceCriteriaSql

Formato de saída — format

O campo format decide como os valores do SQL são emitidos e quais campos a resposta traz. É opcional; se omitido, o valor efetivo é parameterized.

formatComo emite os valoresCampos extras na resposta
parameterizedPlaceholders nomeados @pN no query; o binding vai separado.params, types
literalOs valores são inlineados como literais seguros dentro do query.(nenhum)
  • Em generate-sql (POST) o format vai no corpo.
  • Em :id/sql (GET) o format vai como query param.

Só são aceitas as strings "parameterized" e "literal"; qualquer outro valor produz um 400.

Resposta — IAudienceCriteriaSql

É uma união discriminada por format. Ambas as variantes compartilham uma base e diferem nos campos de binding.

Base (comum aos dois formatos):

CampoTipoDescrição
querystringUma cadeia de CTEs intersectável (uma única string; as cláusulas são separadas por quebras \n): sem o WITH inicial e sem o SELECT final, para inseri-la dentro de um WITH … SELECT.
finalCteNamestringNome do CTE final da cadeia (estável: CTE_AUDIENCE_USERS); é o "handle" do resultado da audiência — um consumidor faz JOIN {finalCteName} / SELECT … FROM {finalCteName}.
subjectColumnstringColuna de identidade exposta pelo CTE final (ex.: user_id).
granularity"table_row" | "record_element"O que é uma linha do CTE final. Em uma audiência é sempre table_row: uma linha por linha da tabela, uma por usuário. O outro valor do contrato, record_element (uma linha por elemento de um registro repetido declarado como fonte de linhas), não ocorre em audiências, que não aceitam fontes de linhas.
  • format: "parameterized" — IParameterizedAudienceCriteriaSql:

    CampoTipoDescrição
    format"parameterized"Discriminador.
    querystringCadeia de CTEs com placeholders @pN.
    finalCteNamestringNome do CTE final.
    subjectColumnstringColuna de identidade exposta pelo CTE final.
    granularity"table_row" | "record_element"O que é uma linha do CTE final (em uma audiência, sempre table_row).
    paramsRecord<string, ScalarValue>Binding de cada @pN → seu valor. ScalarValue = string | number | boolean.
    typesRecord<string, string>Tipo de parâmetro do BigQuery de cada @pN (ex.: STRING, INT64, TIMESTAMP, BOOL, FLOAT64, NUMERIC, BIGNUMERIC).
  • format: "literal" — ILiteralAudienceCriteriaSql:

    CampoTipoDescrição
    format"literal"Discriminador.
    querystringCadeia de CTEs com os valores já inlineados como literais.
    finalCteNamestringNome do CTE final.
    subjectColumnstringColuna de identidade exposta pelo CTE final.
    granularity"table_row" | "record_element"O que é uma linha do CTE final (em uma audiência, sempre table_row).

Em parameterized, params e types fazem parte do contrato: o query traz @pN e é não executável sem seu binding e seus tipos. Em literal não há binding —os valores vivem dentro do query— por isso não traz params nem types. Sempre discrimine por format antes de ler params/types.

Exemplo — parameterized (padrão)

Resposta 201 ao POST de exemplo acima (sem 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" }
}

O query é uma única string com quebras \n — uma cadeia de CTEs sem o WITH inicial e sem o SELECT final. Formatado para leitura:

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 é vinculado com params[pN] (seu valor) e types[pN] (seu tipo BigQuery); sem esse binding o query não é executável. p0 é da audiência e p1 da audiência raiz do projeto, que aqui mantém sua definição padrão (veja Interseção com a audiência raiz). O INNER JOIN com user_properties não está na requisição: IN_THE_FIRST mede os primeiros dias de cada usuário a partir da data de cadastro, então a API o adiciona.

Para executá-la stand-alone, envolva a cadeia com o nome do CTE da resposta: WITH {query} SELECT * FROM {finalCteName}. Para intersectá-la contra outra query, insira {query} no seu próprio WITH e faça JOIN {finalCteName} por {subjectColumn}.

Exemplo — literal

A mesma definição com format: "literal" (na 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"
}

Formatado para leitura:

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
)

Os valores ficam inlineados como literais seguros (aqui 30 e TRUE; strings vão escapadas entre aspas simples); não há params nem types.

Interseção com a audiência raiz

Todo projeto tem uma audiência raiz, e toda outra audiência do projeto é avaliada dentro dela. Em ambos os endpoints, o SQL de qualquer audiência que não seja a audiência raiz sai intersectado com a audiência raiz do projeto:

  • A audiência e a audiência raiz mantêm seus próprios CTEs, e o CTE final intersecta o último CTE de cada uma: CTE_AUDIENCE_USERS AS (SELECT * FROM <último CTE da audiência> INTERSECT DISTINCT SELECT * FROM <último CTE da audiência raiz>).
  • No formato parameterized, os parâmetros @pN da audiência vêm primeiro e os da audiência raiz em seguida.
  • Não há interseção quando a audiência compilada é a própria audiência raiz — GET /:id/sql com o id da audiência raiz, ou um rascunho cujo criteriaId é a audiência raiz.
  • A interseção é posicional (SELECT * nos dois lados): a audiência deve projetar as mesmas colunas que a audiência raiz, na mesma ordem e com tipos compatíveis — com a definição padrão da audiência raiz, apenas user_id. Caso contrário, o SQL falha ao ser executado.

Os CTEs intermediários levam no nome um prefixo do id de cada audiência (em um rascunho enviado sem criteriaId, esse prefixo muda a cada requisição), então referencie sempre o resultado por meio de finalCteName.

Por exemplo, uma audiência de usuários com uma transação COMPLETED, em um projeto cuja audiência raiz foi editada para country = 'CL' (formato literal, formatado para leitura):

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
)

Regras de validação

Antes de compilar, a definição passa pelas mesmas duas camadas de validação que Criar, e ambas retornam 400 Bad Request:

  1. Validação do DTO (estrutura, tipos, enums, campos obrigatórios, format válido). message é um array de strings e propriedades desconhecidas são rejeitadas (incluindo name/description).
  2. Validação de negócio (a definição deve gerar SQL válido e coerente com o catálogo). message é um string único com a regra violada.

A validação de negócio roda em ambos os endpoints: generate-sql valida a definição do corpo, e :id/sql valida o critério salvo antes de compilá-lo. O catálogo completo de regras e suas mensagens exatas está em Criar.

Respostas de erro

StatusDescrição
400DTO inválido (campo faltando, tipo incorreto, enum fora do intervalo, format inválido, propriedade desconhecida como name/description) ou uma regra de negócio violada. Em :id/sql, também se o id não for um UUID válido; em generate-sql, se o criteriaId não for.
401Falta ou é inválido o Authorization: Bearer.
403O usuário não tem a permissão audience-criteria:view-sql, ou o x-tenant-id não está autorizado.
404Em :id/sql, não existe um audience criteria com esse id no projeto ativo. Em generate-sql, um operandCriteriaId de composedWith ou o criteriaId não existe no projeto ativo.