Reten Docs

Definition

The full definition tree of an event filter — query blocks, projection, column, record and related table filters, time window and aggregation.

What this page is

The definition of an event filter is the tree that describes which events it selects. It is identical in every endpoint that receives it or returns it, so it is documented only once here:

EndpointWhere the definition appears
POST /event-criteriasIn the body, under queryBlocks
PATCH /event-criterias/:idIn the body, under queryBlocks (optional)
GET /event-criterias/:idIn the response, under queryBlocks
POST /generate-sqlIn the body, under queryBlocks
POST /validate-from-bodyIn the body, under queryBlocks
POST /preview-from-bodyIn the body, under queryBlocks

The table and column names that appear in the examples come from the catalog, which is the source of what can be filtered.

queryBlocks[] — query block

Minimum 1 element. Each block (QueryBlockDto) is a complete SQL statement; blocks are combined with each other via setOperator.

FieldTypeRequiredDescription
ordernumber (integer)YesBlock position; integer ≥ 0. Unique within the filter.
setOperatorSetOperatorEnumNoCombines this block with the previous one. Absent in the order = 0 block; required in the rest.
fromFromDtoYesData source: main table + joins.
selectSelectDtoYesProjection (columns and/or aggregations).
timeWindowTimeWindowDtoNoBlock time window (discriminated union by timeOperator).
whereWhereDtoYesFilter tree. Required even if it is an empty group.
groupByColumnRefDto[]NoGrouping columns.
havingHavingDtoNoFilter over aggregates.

An event filter is not composed with other filters: there is no composedWith. A block's setOperator combines blocks within the same filter, and it is the only level of set operation there is.

ColumnRefDto — column reference

Used throughout the tree (select, column filters, group by, joins, aggregates, time window):

FieldTypeRequiredDescription
tablestringYesCatalog table. Non-empty.
columnstringYesColumn. Non-empty. Can be a non-repeated STRUCT path (e.g. "pricing.final_amount").

Every column is always qualified with its table to avoid ambiguity. The exception is in the relative sub-trees —the inside of a record filter or of a related table filter—, where the operand is a bare field with no table (see Relative sub-tree).

from — data source (FromDto)

FieldTypeRequiredDescription
tablestringYesMain catalog table. Non-empty.
joinsJoinDto[]NoJoins with other tables. Default [].

Each JoinDto:

FieldTypeRequiredDescription
typeJoinTypeEnumYesINNER | LEFT | RIGHT | FULL.
tablestringYesTable to join (from the catalog). Non-empty.
onJoinPredicateDto[]YesJoin conditions.

Each JoinPredicateDto is a pair of columns: { left: ColumnRefDto, right: ColumnRefDto }. Both sides must be scalar columns.

Only the tables returned by GET /tables can take part in the from. The target table of a semi-join is not a catalog table: it does not enter the from nor is it the target of a join, and it is only reached with a related table filter.

select — projection (SelectDto)

FieldTypeRequiredDescription
distinctbooleanYesApplies SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]YesNon-empty. Discriminated union by the kind field.

The items are discriminated by kind:

  • Column — kind: "column":

    FieldTypeRequiredDescription
    kind"column"YesDiscriminator.
    columnColumnRefDtoYesProjected column. Must be scalar.
    aliasstringNoMust match ^[A-Za-z_][A-Za-z0-9_]*$.
  • Aggregate — kind: "aggregate":

    FieldTypeRequiredDescription
    kind"aggregate"YesDiscriminator.
    aggregateAggregateDtoYesAggregation expression.
    aliasstringYesHere the alias is required. Same identifier regex.

AggregateDto:

FieldTypeRequiredDescription
aggregationOperatorAggregationOperatorEnumYesCOUNT | SUM | AVG | MIN | MAX.
operandobjectYesWhat is aggregated. Discriminated union on kind: column, star or arithmetic (see below).
distinctbooleanNoApplies DISTINCT within the aggregate. COUNT(DISTINCT *) is not allowed.

operand — what is aggregated — is discriminated by kind:

kindAdditional fieldsMeaning
columncolumn: ColumnRefDtoA catalog column.
star(none)*: counts rows. Only valid with COUNT.
arithmeticarithmeticOperator: ArithmeticOperatorEnum, left, rightBinary expression. Each side is another operand: a column or a nested arithmetic (never star).

ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (a division that yields NULL when the denominator is zero instead of failing the query). The columns of an arithmetic expression must be numeric (INT64, NUMERIC, BIGNUMERIC, FLOAT64), and expressions nest up to 3 levels of arithmetic, counting the outermost one.

A RECORD REPEATED column (attributes, items) cannot be projected, grouped, aggregated or used in a join's ON: it is not compared by itself. Attempting it responds 400 with La columna "<tabla>.<columna>" es una columna repetida y no puede usarse en <contexto>. Its place is the record filter.

where — filter tree (WhereDto)

The where is a recursive tree. The root node is always a group, and its leaves come in three types:

kindWhat it filters
groupNot a leaf: it groups children with AND / OR. Groups can be nested.
predicateA scalar column of a table in the from.
recordPredicateA RECORD REPEATED column: there is ≥1 element that satisfies the sub-filter.
semiJoinPredicateA semi-join from the catalog: there is ≥1 match in the related table.

Group — kind: "group"

FieldTypeRequiredDescription
kind"group"YesDiscriminator.
logicalOperatorLogicalOperatorEnum (AND | OR)YesConnective between the children.
children(Group | Leaf)[]YesGroup children; they can be nested groups.

logicalOperator is always required in a group, but it only takes effect with 2 or more children. With fewer than 2 children its value is irrelevant (it generates the same SQL), but it must be sent anyway. A where with no filters is expressed as an empty group: { "kind": "group", "logicalOperator": "AND", "children": [] }.

Column leaf — kind: "predicate"

Compares a scalar column qualified by its table.

FieldTypeRequiredDescription
kind"predicate"YesDiscriminator.
columnColumnRefDtoYesPredicate column.
comparisonOperatorComparisonOperatorEnumYesComparison operator.
valuePredicateValueDependsIts shape depends on the operator.
{
  "kind": "predicate",
  "column": { "table": "event_transactions", "column": "status" },
  "comparisonOperator": "EQUALS",
  "value": "Facturada"
}

Record leaf — kind: "recordPredicate"

Filters by a RECORD REPEATED column (an array of structs, such as attributes or items). It means there is at least one element of the array that satisfies the sub-filter.

FieldTypeRequiredDescription
kind"recordPredicate"YesDiscriminator.
columnColumnRefDtoYesRECORD REPEATED column of a table in the from.
subWhereRelative sub-treeYesConditions over the element's sub-fields. It cannot be left empty.
{
  "kind": "recordPredicate",
  "column": { "table": "event_transactions", "column": "attributes" },
  "subWhere": {
    "kind": "group",
    "logicalOperator": "AND",
    "children": [
      { "kind": "predicate", "field": "key", "comparisonOperator": "EQUALS", "value": "documento_tipo" },
      { "kind": "predicate", "field": "value", "comparisonOperator": "EQUALS", "value": "factura" }
    ]
  }
}

The subWhere conditions are evaluated over one and the same element of the array, not over the whole array. The example above selects events that have an attribute whose key is documento_tipo and whose value is factura — not events that have one attribute with that key and another, different one, with that value.

Filters events by the existence of a match in another table, following a semi-join that the catalog declares. The path between the two tables is resolved by the catalog: the request only names the owner table and the semi-join.

FieldTypeRequiredDescription
kind"semiJoinPredicate"YesDiscriminator.
tablestringYesTable that owns the semi-join. It must take part in the block's from.
semiJoinstringYesName of the semi-join that this table declares in the catalog.
subWhereRelative sub-treeYesConditions over the related table's fields. It cannot be left empty.
{
  "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"]
      }
    ]
  }
}

A semi-join cannot be nested inside another one: the subWhere of a related table leaf accepts scalar and record leaves, but not another semiJoinPredicate leaf.

Relative sub-tree

The subWhere of a record filter and that of a related table filter have the same shape: an AND/OR group whose leaves are resolved against the fields that the scope declares —the record's sub-fields, or the related table's fields.

There is a single difference with the top-level tree, and it is the most common source of error:

Inside a relative sub-tree the operands carry no table: you write "field": "item_id", not "column": { "table": "items", "column": "item_id" }. The leaf uses the field key (a string), not column.

Leaves accepted inside a relative sub-tree:

  • Scalar leaf — { "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. The field must be a scalar sub-field of the scope.
  • Nested record — { "kind": "recordPredicate", "field": string, "subWhere": … }. A record inside another record (for example items[].attributes[]); it nests with no declared limit, as long as the catalog declares each level.
  • Group — the same group shape, to combine the previous leaves with AND/OR.

Shape of value by comparisonOperator

OperatorsShape of valueExample
IS_NULL, IS_NOT_NULLNo value (omitted)(omitted)
IN, NOT_INNon-empty list ScalarValue[]["Food Service", "HORECA"]
BETWEENPair [min, max][10, 20]
The rest (EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH)Single scalar"Facturada" · 42

ScalarValue = string | number | boolean. The rule holds equally for column leaves and for the scalar leaves of a relative sub-tree.

Operators accepted by column type

Each operator must also be compatible with the column type that the catalog declares. An operator outside this table responds 400 with Operator <op> is not allowed for <type> operands.

Column typeAccepted operators
STRINGEQUALS · NOT_EQUALS · CONTAINS · NOT_CONTAINS · STARTS_WITH · ENDS_WITH · IN · NOT_IN · IS_NULL · IS_NOT_NULL
BOOLEANEQUALS · NOT_EQUALS · IS_NULL · IS_NOT_NULL
FLOAT64, INT64, NUMERIC, BIGNUMERICEQUALS · NOT_EQUALS · GREATER_THAN · LESS_THAN · GREATER_THAN_OR_EQUAL · LESS_THAN_OR_EQUAL · BETWEEN · IN · NOT_IN · IS_NULL · IS_NOT_NULL
TIMESTAMPEQUALS · NOT_EQUALS · GREATER_THAN · LESS_THAN · GREATER_THAN_OR_EQUAL · LESS_THAN_OR_EQUAL · BETWEEN · IS_NULL · IS_NOT_NULL

Filtering by date range does not go through these operators: it is covered by the timeWindow field.

groupBy — grouping columns

Optional list of ColumnRefDto[]. When the block aggregates (declares groupBy or has an aggregate in the select), the groupBy must match exactly the non-aggregated columns of the select.

having — filter over aggregates (HavingDto)

Same recursive structure as the where —nested AND/OR groups and the same value rule per operator—, but the leaf operates over an aggregate, not over a column:

  • Group — identical to the one in the where.
  • Leaf — { "kind": "predicate", "aggregate": AggregateDto, "comparisonOperator": …, "value"?: … }.

having requires an aggregation context: the block must declare groupBy or have an aggregate in the select. Without that it responds 400 with HAVING requires an aggregation context (GROUP BY or an aggregate in the SELECT).

timeWindow — time window (TimeWindowDto)

Discriminated union by timeOperator. Constrains the block to a time period over a TIMESTAMP column from the catalog (reference). It is a sibling field of the where, not a node of the filter tree.

timeOperatorAdditional fields
ALL_TIME(none) — no time constraint.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 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.

Semantics of each operator:

  • ALL_TIME — no window; considers the entire history.
  • IN_THE_LAST — rolling window relative to "now": the last value unit (e.g. last 30 DAYS). Recomputed on each run.
  • SINCE — from the civil date from (inclusive) onward.
  • BETWEEN — between from and to, both civil dates inclusive of their full day.

IN_THE_FIRST is a valid member of TimeOperatorEnum, but it is not supported in this resource: it measures the life of a subject and an event filter does not have one. Sending it responds 400 with timeWindow IN_THE_FIRST is not supported: the catalog does not declare a lifetime origin.

Civil dates YYYY-MM-DD (SINCE, BETWEEN) are resolved to the start of the day in the tenant's timezone. The timezone detail is internal; it is enough to send the civil date.

Validation Rules

The definition goes through two layers of validation, both returning 400 Bad Request:

  1. DTO validation (structure, types, enums, required fields). message is an array of strings and unknown properties are rejected.
  2. Business validation (the definition must generate valid SQL that is coherent against the catalog). message is a single string with the message of the violated rule, and it stops at the first violated invariant.

Business rules verified, with the exact message from the API:

Catalog and references

RuleMessage
The table must belong to the catalog.Table "<t>" is not in the catalog
Each (table, column) must belong to the catalog.Column "<t>.<c>" is not in the catalog
Every referenced column must belong to a table in the from.Column "<t>.<c>" references a table that is not in the query block FROM
A repeated column cannot be used outside a record filter.La columna "<t>.<c>" es una columna repetida y no puede usarse en <contexto>
A leaf of the tree must declare its operand.Una hoja del filtro no declara una columna

Projection and aggregation

RuleMessage
The star operand (*) is only valid with COUNT.'*' is only valid with COUNT, not <op>
COUNT(DISTINCT *) is not allowed.COUNT(DISTINCT *) is not allowed
star cannot be a side of an arithmetic expression.'*' sólo vale como operando directo de COUNT, no como lado de una expresión aritmética
The columns of an arithmetic expression must be numeric.El operando "<t>.<c>" es de tipo <type>; una expresión aritmética sólo admite operandos numéricos (INT64, NUMERIC, BIGNUMERIC, FLOAT64)
An arithmetic expression nests at most 3 levels.La expresión aritmética supera la profundidad máxima de 3 niveles
When aggregating, every non-aggregated column of the select must be in the groupBy.Column "<t>.<c>" must appear in GROUP BY when the operation aggregates
Every column in the groupBy must be a non-aggregated column of the select.Column "<t>.<c>" in GROUP BY must be a non-aggregated column of the SELECT
having requires an aggregation context.HAVING requires an aggregation context (GROUP BY or an aggregate in the SELECT)

Query blocks

RuleMessage
There must be at least one block.A criteria must declare at least one operation (query block)
The order values are unique.Two operations declare the same order=<n>: each operation order must be unique
The order = 0 block must exist.Missing the first operation: one operation must declare order=0
The first block does not carry a setOperator.First operation (order=0) must not have a setOperator
Every subsequent block must carry a setOperator.Operation with order=<n> must have a setOperator

Operators and values

RuleMessage
The operator must be compatible with the column/aggregate type.Operator <op> is not allowed for <type> operands
Scalar operator with a missing value or a list.Operator <op> requires a single value
IN/NOT_IN with a non-list or empty value.Operator <op> requires a non-empty list of values
BETWEEN without exactly two values.Operator <op> requires exactly two values
RuleMessage
A record filter requires a repeated column.La columna "<t>.<c>" no es una columna repetida: no admite un filtro de registro
A column filter requires a scalar column.La columna "<t>.<c>" es una columna repetida: no admite un filtro de columna
The sub-field must exist in the record.El sub-campo "<f>" no existe en el registro
The sub-field must exist in the related table.El sub-campo "<f>" no existe en la tabla relacionada "<t>"
A scalar leaf cannot point at a repeated sub-field.El sub-campo "<f>" es una columna repetida: usá un filtro de registro anidado
A nested record requires a repeated sub-field.El sub-campo "<f>" no es una columna repetida
The sub-tree of a record filter cannot be left empty.Un filtro de registro no puede quedar sin condiciones
The sub-tree of a related table filter cannot be left empty.Un filtro sobre la tabla relacionada "<t>" no puede quedar sin condiciones
The table that owns the semi-join must take part in the from.El semi-join "<s>" se declara sobre la tabla "<t>", que no participa del FROM del bloque
The semi-join must be declared by that table in the catalog.El semi-join "<s>" no está declarado en la tabla "<t>"

Time window

RuleMessage
The reference must be in the catalog.timeWindow reference "<t>.<c>" is not in the catalog
The reference must be a TIMESTAMP column.timeWindow reference "<t>.<c>" must be a TIMESTAMP column
The reference cannot be a repeated column.La referencia temporal "<t>.<c>" es una columna repetida y no puede anclar una ventana de tiempo
value must be a positive integer.timeWindow value must be a positive integer
unit must be a member of TimeUnitEnum.timeWindow unit "<u>" is not a valid time unit
In BETWEEN, from must be on or before to.timeWindow BETWEEN requires "from" to be on or before "to"
Dates must be valid calendar dates.timeWindow date "<d>" is not a valid calendar date
IN_THE_FIRST is not supported.timeWindow IN_THE_FIRST is not supported: the catalog does not declare a lifetime origin