Reten Docs
Audiências

Validar

Valida uma definição de audience criteria e executa um dry-run contra o BigQuery, a partir do corpo (ainda não persistida) ou por id de um critério salvo; retorna se é válida e o custo estimado, sem dados nem SQL.

Validar

Valida uma definição de audience criteria e, se passar em todas as validações, executa um dry-run contra o BigQuery. Um mesmo motor atende duas entradas:

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

Ambos retornam a mesma forma de resposta: IValidateAudienceCriteriaResult, com a validade e o custo estimado do query. A validação não retorna linhas nem o SQL gerado: apenas se a definição é executável e quantos bytes ela processaria.

Dentro de um projeto, toda audiência que não seja a audiência raiz é validada intersectada 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).

Distinga dois caminhos de falha que este recurso trata de forma diferente:

  • A validação estrutural e de negócio (DTO, catálogo, regras de agregação, janela temporal, etc.) ocorre antes do dry-run e, se falhar, responde com um erro HTTP (400/404) — não chega ao corpo da resposta.
  • O dry-run do BigQuery é a única coisa refletida no corpo como valid: false (com status 201).

Ver Validação estrutural vs. dry-run.


POST /api/audience-criterias/validate-from-body

Valida 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 (CriteriaDefinitionDto): a definição do critério, sem wrapper.

CampoTipoObrigatórioDescrição
queryBlocksQueryBlockDto[]SimPelo menos 1 elemento. Cada bloco ≈ um statement SQL completo.
composedWithCreateCompositionDto[]NãoCompõe esta definição com outras audiências via operações de conjunto.
criteriaIdstring (UUID) | nullNãoA audiência salva que esta definição edita: o rascunho assume a identidade dessa audiência, então gera o mesmo resultado 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. Também não existe aqui o campo format de Gerar SQL —validar não emite SQL—. A validação do DTO é estrita, então enviar name/description/format (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 é indiferente (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) se resolvem 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 3 ou mais transações COMPLETED nos últimos 90 dias.

curl -X POST https://api.reten.ai/api/audience-criterias/validate-from-body \
  -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": false,
          "items": [
            {
              "kind": "column",
              "column": { "table": "event_transactions", "column": "user_id" }
            }
          ]
        },
        "timeWindow": {
          "timeOperator": "IN_THE_LAST",
          "reference": { "table": "event_transactions", "column": "event_date" },
          "unit": "DAYS",
          "value": 90
        },
        "where": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "column": { "table": "event_transactions", "column": "status" },
              "comparisonOperator": "EQUALS",
              "value": "COMPLETED"
            }
          ]
        },
        "groupBy": [{ "table": "event_transactions", "column": "user_id" }],
        "having": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "aggregate": {
                "aggregationOperator": "COUNT",
                "operand": { "kind": "star" }
              },
              "comparisonOperator": "GREATER_THAN_OR_EQUAL",
              "value": 3
            }
          ]
        }
      }
    ]
  }'
import axios from 'axios';

const body = {
  queryBlocks: [
    {
      order: 0,
      from: { table: 'event_transactions' },
      select: {
        distinct: false,
        items: [
          {
            kind: 'column',
            column: { table: 'event_transactions', column: 'user_id' },
          },
        ],
      },
      timeWindow: {
        timeOperator: 'IN_THE_LAST',
        reference: { table: 'event_transactions', column: 'event_date' },
        unit: 'DAYS',
        value: 90,
      },
      where: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            column: { table: 'event_transactions', column: 'status' },
            comparisonOperator: 'EQUALS',
            value: 'COMPLETED',
          },
        ],
      },
      groupBy: [{ table: 'event_transactions', column: 'user_id' }],
      having: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            aggregate: {
              aggregationOperator: 'COUNT',
              operand: { kind: 'star' },
            },
            comparisonOperator: 'GREATER_THAN_OR_EQUAL',
            value: 3,
          },
        ],
      },
    },
  ],
};

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

A resposta é documentada em IValidateAudienceCriteriaResult, com um exemplo válido e um inválido.


POST /api/audience-criterias/:id/validate

Valida 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

Assim como validate-from-body, o endpoint responde 201 Created —não 200—, mesmo sem criar nenhum recurso.

Parâmetros de rota

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

Este endpoint não recebe corpo nem parâmetros de consulta.

Exemplo

curl -X POST https://api.reten.ai/api/audience-criterias/f47ac10b-58cc-4372-a567-0e02b2c3d479/validate \
  -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.post(
  `https://api.reten.ai/api/audience-criterias/${id}/validate`,
  undefined, // sem corpo
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const result = response.data; // IValidateAudienceCriteriaResult

Resposta — IValidateAudienceCriteriaResult

Status 201 Created em ambos os endpoints. O corpo relata o resultado do dry-run do BigQuery:

CampoTipoObrigatórioDescrição
validbooleanSimtrue se o dry-run teve sucesso; false se o BigQuery rejeitou o query.
errorsstring[]NãoPresente somente quando valid: false. Contém a(s) mensagem(ns) de erro do BigQuery.
bytesProcessednumberSimBytes que o BigQuery estima processar. Em caso de sucesso, a estimativa do dry-run; em caso de falha, 0.

O dry-run não executa o query nem retorna linhas: apenas o valida contra o BigQuery e estima seu custo (bytesProcessed). Para obter uma amostra de linhas reais use Pré-visualizar; para ver o SQL gerado, Gerar SQL.

errors é um array por contrato, mas na prática o dry-run retorna uma única mensagem (a que o BigQuery reporta). Sempre discrimine por valid antes de ler errors.

Exemplo — válida (valid: true)

Uma resposta 201 para uma definição que compila e passa no dry-run:

{
  "valid": true,
  "bytesProcessed": 10485760
}

Quando valid é true, errors é omitido e bytesProcessed traz a estimativa do BigQuery (neste exemplo, ~10 MB).

Exemplo — inválida (valid: false)

A definição passou na validação estrutural e de negócio e compilou para SQL, mas o dry-run do BigQuery a rejeitou (ex.: uma coluna do catálogo que ainda não existe na tabela real do dataset, ou um query cujo custo estimado excede o limite de bytes faturáveis). Ainda assim o status é 201:

{
  "valid": false,
  "errors": [
    "Unrecognized name: legacy_column at [3:8]"
  ],
  "bytesProcessed": 0
}

Com valid: false, errors traz a mensagem do BigQuery e bytesProcessed é 0.


Interseção com a audiência raiz

Todo projeto tem uma audiência raiz, e toda outra audiência do projeto é avaliada dentro dela. O dry-run roda sobre o mesmo SQL que Gerar SQL retorna: para qualquer audiência que não seja a audiência raiz, intersectado com a audiência raiz do projeto. Por isso valid e bytesProcessed se referem a essa consulta intersectada, e a estimativa inclui a parte da audiência raiz.

Não há interseção quando a audiência validada é a própria audiência raiz — :id/validate com o id da audiência raiz, ou um rascunho de validate-from-body 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 BigQuery rejeita a consulta intersectada no dry-run e a resposta é valid: false, com a mensagem do BigQuery em errors.

Validação estrutural vs. dry-run

Este é o ponto-chave do recurso: nem todas as falhas chegam a errors[]. Uma requisição passa por etapas e cada uma falha de forma diferente.

EtapaComo falhaOnde aparece
1. Validação do DTO (estrutura, tipos, enums)Erro HTTP 400message (array de strings); interrompe a requisição.
2. Resolução do critério (somente :id, criteriaId e composedWith)Erro HTTP 404message (string); interrompe a requisição.
3. Validação de negócio (catálogo, agregação, tempo)Erro HTTP 400message (string); interrompe a requisição.
4. Dry-run do BigQueryvalid: false (status 201)Campo errors do corpo de resposta.

Em outras palavras:

  • Tudo o que acontece antes do dry-run (etapas 1–3) que falha responde com um erro HTTP e não produz um corpo IValidateAudienceCriteriaResult. Por exemplo: uma tabela fora do catálogo, um critério que não projeta sua coluna de identidade, um timeWindow malformado, um id ou um criteriaId inexistente, ou um operandCriteriaId que não existe.
  • valid: false é exclusivo do dry-run: a definição já era estruturalmente válida e compilou para SQL, mas o BigQuery a rejeitou ao estimá-la. O status continua sendo 201.

Não assuma que um "critério inválido" sempre chega como valid: false. A maioria dos erros de forma e de negócio são erros HTTP 400/404 e nunca alcançam o dry-run. valid: false significa especificamente que o BigQuery rejeitou um query que já havia passado por toda a validação local.

Regras de validação

Antes do dry-run, 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). message é um array de strings e propriedades desconhecidas são rejeitadas (incluindo name/description/format).
  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, e para no primeiro invariante violado.

A validação de negócio roda em ambos os endpoints: validate-from-body valida a definição do corpo, e :id/validate 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

Estes são os erros HTTP (diferentes de valid: false, que viaja com status 201):

StatusDescrição
400DTO inválido (campo faltando, tipo incorreto, enum fora do intervalo, propriedade desconhecida como name/description/format) ou uma regra de negócio violada. Em :id/validate, também se o id não for um UUID válido; em validate-from-body, 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/validate, não existe um audience criteria com esse id no projeto ativo. Em validate-from-body, um operandCriteriaId de composedWith ou o criteriaId não existe no projeto ativo.