Generate SQL
Compiles an audience criteria definition to BigQuery SQL, from the request body (not yet persisted) or by id of a saved criterion, in parameterized or literal format.
Generate SQL
Compiles an audience criteria definition to BigQuery SQL (GoogleSQL). A single engine serves two entry points:
POST /api/audience-criterias/generate-sql— compiles a definition sent in the body (not yet persisted).GET /api/audience-criterias/:id/sql— compiles an already saved criterion, referenced by itsid.
Both return the same response shape: IAudienceCriteriaSql,
a discriminated union on the format field. The format (parameterized or literal)
is chosen per request and governs both the shape of the query and the fields of the
response.
Within a project, the SQL of every audience other than the root audience comes out intersected with the project's root audience — see Intersection with the root audience.
Auth: Required — audience-criteria:view-sql permission (both endpoints).
The generated SQL is meant for BigQuery: it references tables as
`<gcp-project>.<dataset>.<table>`. The query is an intersectable chain of CTEs —
without the leading WITH and without a trailing SELECT — so a consumer can splice
it straight into its own WITH … SELECT. The last CTE is named finalCteName
(CTE_AUDIENCE_USERS) and exposes the identity column subjectColumn (e.g. user_id). In
the parameterized format it uses named @pN parameters. The <gcp-project> (the Google Cloud
project, not a Reten project) comes from the API's environment configuration and the <dataset> is resolved from the tenant; neither is part of
the input contract.
POST /api/audience-criterias/generate-sql
Compiles a definition sent in the body, without persisting it.
Auth: Required — audience-criteria:view-sql permission
The endpoint responds 201 Created —not 200—, even though it does not create
any resource.
Request Body
Root level (GenerateSqlDto): the criterion definition (CriteriaDefinitionDto)
plus the format field.
| Field | Type | Required | Description |
|---|---|---|---|
queryBlocks | QueryBlockDto[] | Yes | Minimum 1 element. Each block ≈ a complete SQL statement. |
composedWith | CreateCompositionDto[] | No | Composes this definition with other audiences via set operations. |
format | "parameterized" | "literal" | No | Output format. Default parameterized (see Output format). |
criteriaId | string (UUID) | null | No | The saved audience this definition edits, so the draft gives the same SQL that audience will give once saved (see Intersection with the root audience). Absent or null: a new audience. It must be a valid UUID (otherwise, 400) of an audience of the active project (otherwise, 404). |
Unlike Create, the definition
does not carry name or description: it is only the query tree. Validation is
strict, so sending name/description (or any other unknown property) produces
a 400.
Criterion Definition
It is the same tree that Create receives, without the
name/description fields. It is documented here in a self-contained way.
queryBlocks[] — query block
Each element of queryBlocks (QueryBlockDto) is a complete SQL statement:
| Field | Type | Required | Description |
|---|---|---|---|
order | number (integer) | Yes | Block position; integer ≥ 0. |
setOperator | SetOperatorEnum | No | Combines this block with the previous one. Absent in the first block; required in the rest (see rules). |
from | FromDto | Yes | Data source: main table + joins. |
select | SelectDto | Yes | Projection (columns and/or aggregations). |
timeWindow | TimeWindowDto | No | Block time window (discriminated union on timeOperator). |
where | WhereDto | Yes | Predicate tree. Required even if it is an empty group. |
groupBy | ColumnRefDto[] | No | Grouping columns. |
having | HavingDto | No | Filter on aggregates. |
The setOperator of a query block combines blocks within the same definition;
it should not be confused with the setOperator of a composition,
which combines whole criteria. They are two distinct levels of set operation.
ColumnRefDto — column reference
Used throughout the tree (select, filters, group by, joins, aggregates, time window):
| Field | Type | Required | Description |
|---|---|---|---|
table | string | Yes | Catalog table. Not empty. |
column | string | Yes | Column. Not empty. Can be a STRUCT path (e.g. "pricing.final_amount"). |
Every column is always qualified with its table to avoid ambiguity.
from — data source (FromDto)
| Field | Type | Required | Description |
|---|---|---|---|
table | string | Yes | Main catalog table. Not empty. |
joins | JoinDto[] | No | Joins with other tables. Default []. |
unnests | ColumnRefDto[] | No | Repeated columns declared as a row source (one row per element). An audience does not accept them: omit the field or send [] (the default); a non-empty list gives 400. |
Each JoinDto:
| Field | Type | Required | Description |
|---|---|---|---|
type | JoinTypeEnum | Yes | INNER | LEFT | RIGHT | FULL. |
table | string | Yes | Table to join (from the catalog). Not empty. |
on | JoinPredicateDto[] | Yes | Join conditions. |
Each JoinPredicateDto is a pair of columns: { left: ColumnRefDto, right: ColumnRefDto }.
select — projection (SelectDto)
| Field | Type | Required | Description |
|---|---|---|---|
distinct | boolean | Yes | Applies SELECT DISTINCT. |
items | (SelectColumnItem | SelectAggregateItem)[] | Yes | Not empty. Discriminated union on the kind field. |
The items are discriminated by kind:
-
Column —
kind: "column":Field Type Required Description kind"column"Yes Discriminator. columnColumnRefDtoYes Projected column. aliasstring No Must match ^[A-Za-z_][A-Za-z0-9_]*$. -
Aggregate —
kind: "aggregate":Field Type Required Description kind"aggregate"Yes Discriminator. aggregateAggregateDtoYes Aggregation expression. aliasstring Yes Here the alias is required. Same identifier regex.
AggregateDto:
| Field | Type | Required | Description |
|---|---|---|---|
aggregationOperator | AggregationOperatorEnum | Yes | COUNT | SUM | AVG | MIN | MAX. |
operand | object | Yes | What is aggregated. Discriminated union on kind: column, star or arithmetic (see below). |
distinct | boolean | No | Applies DISTINCT inside the aggregate. |
operand — what is aggregated — is discriminated by kind:
kind | Additional fields | Meaning |
|---|---|---|
column | column: ColumnRefDto | A catalog column. |
star | (none) | *: counts rows. Only valid with COUNT and without distinct (COUNT(DISTINCT *) is not allowed). |
arithmetic | arithmeticOperator: ArithmeticOperatorEnum, left, right | Binary 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.
where — predicate tree (WhereDto)
The where is a recursive tree. The root node is always a group, and its leaves come in
two kinds:
kind | What it filters |
|---|---|
group | Not a leaf: it groups children with AND / OR. Groups can be nested. |
predicate | A scalar column of a table in the from. |
recordPredicate | A RECORD REPEATED column: at least 1 element satisfies the sub-filter. |
Group — kind: "group"
| Field | Type | Required | Description |
|---|---|---|---|
kind | "group" | Yes | Discriminator. |
logicalOperator | LogicalOperatorEnum (AND | OR) | Yes | Connective between the children. |
children | (Group | Leaf)[] | Yes | Group children; they can be nested groups. |
logicalOperator is always required in a group, but only takes effect with
2 or more children. With fewer than 2 children its value is irrelevant (generates the same SQL), but
it must be sent anyway. A where without filters is expressed as an empty group:
{ "kind": "group", "logicalOperator": "AND", "children": [] }.
Column leaf — kind: "predicate"
Compares a scalar column qualified by its table.
| Field | Type | Required | Description |
|---|---|---|---|
kind | "predicate" | Yes | Discriminator. |
column | ColumnRefDto | Yes | Predicate column. |
comparisonOperator | ComparisonOperatorEnum | Yes | Comparison operator. |
value | PredicateValue | Depends | Its shape depends on the operator. |
{
"kind": "predicate",
"column": { "table": "user_properties", "column": "country" },
"comparisonOperator": "IN",
"value": ["CL", "MX"]
}Record leaf — kind: "recordPredicate"
Filters by a RECORD REPEATED column (an array of structs, such as attributes).
It means at least one element of the array satisfies the sub-filter.
| Field | Type | Required | Description |
|---|---|---|---|
kind | "recordPredicate" | Yes | Discriminator. |
column | ColumnRefDto | Yes | RECORD REPEATED column of a table in the from. |
subWhere | Relative sub-tree | Yes | Conditions 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": "canal" },
{ "kind": "predicate", "field": "value", "comparisonOperator": "EQUALS", "value": "web" }
]
}
}The subWhere conditions are evaluated over a single element of the array, not over
the whole array. The example above selects rows that have an attribute whose key is
canal and whose value is web — not rows that have an attribute with that key
and another, different one, with that value.
Which columns accept a record filter is stated by the
catalog: the ones that come with
kind: "record".
Relative sub-tree
The subWhere is an AND/OR group whose leaves are resolved against the sub-fields the
record declares.
There is a single difference with the top-level tree, and it is the most common source of errors:
Inside a relative sub-tree the operands carry no table: you write
"field": "key", not "column": { "table": "attributes", "column": "key" }. The leaf uses
the field key (a string), not column.
Leaves allowed inside a relative sub-tree:
- Scalar leaf —
{ "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. Thefieldmust be a scalar sub-field of the record. - Nested record —
{ "kind": "recordPredicate", "field": string, "subWhere": … }. A record inside another record; 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.
value shape by comparisonOperator
| Operators | value shape | Example |
|---|---|---|
IS_NULL, IS_NOT_NULL | No value (omitted) | (omitted) |
IN, NOT_IN | Non-empty list ScalarValue[] | ["CL", "MX"] |
BETWEEN | Pair [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 | "active" · 42 |
ScalarValue = string | number | boolean. In addition, each operator must be
compatible with the column's type (see validation rules).
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 (see validation rules).
having — filter on aggregates (HavingDto)
Same recursive structure as where (nested AND/OR groups and the same value rule
per operator), but the leaf operates on an aggregate, not on a column:
- Predicate (leaf) —
{ kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }. - Group — identical to the one in
where.
timeWindow — time window (TimeWindowDto)
Discriminated union on timeOperator. Narrows the block to a time period over a
TIMESTAMP column from the catalog (reference). It is a sibling field of where, not a
node of the filter tree.
timeOperator | Additional fields |
|---|---|
ALL_TIME | (none) — not bounded by time. |
IN_THE_LAST | reference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 0 |
IN_THE_FIRST | reference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 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.
Business semantics of each operator:
ALL_TIME— no window; considers the entire history.IN_THE_LAST— rolling window relative to "now": the lastvalueunit(e.g. last30 DAYS). Recomputed on each run.IN_THE_FIRST— window relative to the origin of the user's lifecycle (their signup date): the firstvalueunitof each user's life. Anchored per user, not to a fixed date.SINCE— from the civil datefrom(inclusive) onward.BETWEEN— betweenfromandto, both civil dates inclusive of their full day.
The civil dates YYYY-MM-DD (SINCE, BETWEEN) are resolved to the start of the day in
the tenant's time zone. The time-zone detail is internal; sending the civil
date is enough.
composedWith[] — composition with other audiences
Each element (CreateCompositionDto) combines this definition with another audience
via a set operation:
| Field | Type | Required | Description |
|---|---|---|---|
operandCriteriaId | string (UUID) | Yes | Id of the other audience (operand). Must exist. |
setOperator | SetOperatorEnum | Yes | UNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT. |
Example
Definition of an audience of users with a transaction within their first 30 days since
signup (IN_THE_FIRST). By omitting format, the response is parameterized (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', // optional; defaults to '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; // IAudienceCriteriaSqlThe response is documented in IAudienceCriteriaSql, with
an example of each format.
GET /api/audience-criterias/:id/sql
Compiles an already saved criterion, referenced by its id. The definition does not travel in
the body: it is taken from the criterion persisted in the active project.
Auth: Required — audience-criteria:view-sql permission
Status 200 OK (it is a GET).
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | Id of the audience criteria. Must be a valid UUID (otherwise, 400). |
Query Parameters
GetSqlDto:
| Parameter | Type | Required | Description |
|---|---|---|---|
format | "parameterized" | "literal" | No | Output format. Default parameterized (see Output format). |
Example
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' }, // optional; defaults to 'parameterized'
headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' },
},
);
const sql = response.data; // IAudienceCriteriaSqlOutput format — format
The format field decides how the SQL values are emitted and which fields the
response carries. It is optional; if omitted, the effective value is parameterized.
format | How it emits values | Extra fields in the response |
|---|---|---|
parameterized | Named @pN placeholders in the query; the binding goes separately. | params, types |
literal | Values are inlined as safe literals inside the query. | (none) |
- In
generate-sql(POST) theformatgoes in the body. - In
:id/sql(GET) theformatgoes as a query param.
Only the strings "parameterized" and "literal" are accepted; any other value produces
a 400.
Response — IAudienceCriteriaSql
It is a discriminated union on format. Both variants share a base and differ
in the binding fields.
Base (common to both formats):
| Field | Type | Description |
|---|---|---|
query | string | An intersectable chain of CTEs (single string, clauses separated by \n): without the leading WITH and without a trailing SELECT, so it can be spliced into a WITH … SELECT. |
finalCteName | string | Name of the final CTE of the chain (stable: CTE_AUDIENCE_USERS); the "handle" of the audience result — a consumer does JOIN {finalCteName} / SELECT … FROM {finalCteName}. |
subjectColumn | string | Identity column exposed by the final CTE (e.g. user_id). |
granularity | "table_row" | "record_element" | What one row of the final CTE is. In an audience it is always table_row: one row per table row, one per user. The contract's other value, record_element (one row per element of a repeated record declared as a row source), does not occur in audiences, which do not accept row sources. |
-
format: "parameterized"—IParameterizedAudienceCriteriaSql:Field Type Description format"parameterized"Discriminator. querystring CTE chain with @pNplaceholders.finalCteNamestring Name of the final CTE. subjectColumnstring Identity column exposed by the final CTE. granularity"table_row"|"record_element"What one row of the final CTE is (in an audience, always table_row).paramsRecord<string, ScalarValue>Binding of each @pN→ its value.ScalarValue=string|number|boolean.typesRecord<string, string>BigQuery parameter type of each @pN(e.g.STRING,INT64,TIMESTAMP,BOOL,FLOAT64,NUMERIC,BIGNUMERIC). -
format: "literal"—ILiteralAudienceCriteriaSql:Field Type Description format"literal"Discriminator. querystring CTE chain with the values already inlined as literals. finalCteNamestring Name of the final CTE. subjectColumnstring Identity column exposed by the final CTE. granularity"table_row"|"record_element"What one row of the final CTE is (in an audience, always table_row).
In parameterized, params and types are part of the contract: the query carries
@pN and is not executable without its binding and its types. In literal there is no binding —the
values live inside the query— so it does not carry params or types. Always discriminate
by format before reading params/types.
Example — parameterized (default)
201 response to the example POST above (without 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" }
}The query is a single string with \n line breaks — a CTE chain without the leading
WITH and without a trailing SELECT. Formatted for readability:
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
)Each @pN is bound with params[pN] (its value) and types[pN] (its BigQuery type); without that
binding the query is not executable. p0 belongs to the audience and p1 to the project's
root audience, which here keeps its default definition (see
Intersection with the root audience). The INNER JOIN to
user_properties is not in the request: IN_THE_FIRST measures each user's first days from their
signup date, so the API adds it.
To run it stand-alone, wrap the chain with the CTE name from the response:
WITH {query} SELECT * FROM {finalCteName}. To intersect it against another query,
splice {query} into your own WITH and JOIN {finalCteName} on {subjectColumn}.
Example — literal
The same definition with format: "literal" (in the by-id variant, ?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"
}Formatted for readability:
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
)The values are inlined as safe literals (here 30 and TRUE; strings go escaped, in
single quotes); there are no params or types.
Intersection with the root audience
Every project has a root audience, and every other audience of the project is evaluated within it. On both endpoints, the SQL of any audience that is not the root audience comes out intersected with the project's root audience:
- The audience and the root audience keep their own CTEs, and the final CTE intersects the last CTE of each:
CTE_AUDIENCE_USERS AS (SELECT * FROM <audience CTE> INTERSECT DISTINCT SELECT * FROM <root audience CTE>). - In the
parameterizedformat, the audience's@pNparameters come first and the root audience's parameters follow. - There is no intersection when the compiled audience is the root audience itself —
GET /:id/sqlwith the root audience'sid, or a draft whosecriteriaIdis the root audience. - The intersection is positional (
SELECT *on both sides): the audience must project the same columns as the root audience, in the same order and with compatible types — with the root audience's default definition, onlyuser_id. Otherwise, the SQL fails when it runs.
The intermediate CTEs carry a prefix of each audience's id in their names (for a draft sent
without criteriaId, that prefix changes on every request), so always reference the result
through finalCteName.
For example, an audience of users with a COMPLETED transaction, in a project whose root audience
was edited to country = 'CL' (literal format, formatted for readability):
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
)Validation Rules
Before compiling, the definition goes through the same two layers of validation as
Create, and both
return 400 Bad Request:
- DTO validation (structure, types, enums, required fields, valid
format).messageis an array of strings and unknown properties are rejected (includingname/description). - Business validation (the definition must generate valid SQL coherent against the
catalog).
messageis a single string with the violated rule.
Business validation runs on both endpoints: generate-sql validates the
body definition, and :id/sql validates the saved criterion before compiling it. The
full catalog of rules and their exact messages is in
Create → Validation Rules.
Error Responses
| Status | Description |
|---|---|
400 | Invalid DTO (missing field, wrong type, enum out of range, invalid format, unknown property such as name/description) or a violated business rule. In :id/sql, also if the id is not a valid UUID; in generate-sql, if the criteriaId is not. |
401 | Missing or invalid Authorization: Bearer. |
403 | The user does not have the audience-criteria:view-sql permission, or the x-tenant-id is not authorized. |
404 | In :id/sql, no audience criteria with that id exists in the active project. In generate-sql, an operandCriteriaId from composedWith or the criteriaId does not exist in the active project. |