Reten Docs

Generate SQL

Compiles an event filter to the chain of CTEs that represents it, from the body or from a saved filter.

Generate SQL

Compiles an event filter definition to the SQL that represents it. There are two routes with the same response: one compiles a definition sent in the body and the other an event filter that is already saved.

RouteDefinition sourceSuccess code
POST /api/event-criterias/generate-sqlThe body201
GET /api/event-criterias/:id/sqlThe saved filter200

Auth: Required — event-criteria:view-sql permission on both routes

The SQL is not executed: these routes compile and return. To validate it against BigQuery use Validate; to see real rows, Preview.


POST /api/event-criterias/generate-sql

Request Body

FieldTypeRequiredDescription
queryBlocksQueryBlockDto[]YesMinimum 1 element. The definition to compile.
formatstringNoparameterized | literal. Default parameterized.

The structure of each block is in Definition.

This endpoint receives only the definition: name and description are not part of the body and sending them produces a 400 for unknown property.

Example

curl -X POST https://api.reten.ai/api/event-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", "joins": [] },
        "select": {
          "distinct": true,
          "items": [
            { "kind": "column", "column": { "table": "event_transactions", "column": "event_id" } }
          ]
        },
        "where": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "column": { "table": "event_transactions", "column": "status" },
              "comparisonOperator": "EQUALS",
              "value": "Facturada"
            },
            {
              "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"]
                  }
                ]
              }
            }
          ]
        }
      }
    ]
  }'
import axios from 'axios';

const body = {
  queryBlocks: [
    {
      order: 0,
      from: { table: 'event_transactions', joins: [] },
      select: {
        distinct: true,
        items: [
          { kind: 'column', column: { table: 'event_transactions', column: 'event_id' } },
        ],
      },
      where: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            column: { table: 'event_transactions', column: 'status' },
            comparisonOperator: 'EQUALS',
            value: 'Facturada',
          },
          {
            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'],
                },
              ],
            },
          },
        ],
      },
    },
  ],
};

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

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

Compiles an already saved filter. The definition is read from the filter and validated before compiling it.

Path Parameters

ParameterTypeDescription
idUUIDId of the event filter.

Query Parameters

ParameterTypeDefaultDescription
formatstringparameterizedparameterized | literal.

Example

curl "https://api.reten.ai/api/event-criterias/6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a/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 = '6d2a9c41-7e35-48b0-b1c9-0f8e7d6c5b4a';
const response = await axios.get(
  `https://api.reten.ai/api/event-criterias/${id}/sql`,
  {
    headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' },
    params: { format: 'literal' },
  },
);
const sql = response.data;

Output 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.

formatHow it emits valuesExtra fields in the response
parameterizedNamed @pN placeholders in the query; the binding goes separately.params, types
literalValues are inlined as safe literals inside the query.(none)
  • In generate-sql (POST) the format goes in the body.
  • In :id/sql (GET) the format goes as a query param.

Only the strings "parameterized" and "literal" are accepted; any other value produces a 400.

Response — IEventCriteriaSql

It is a discriminated union on format. Both variants share a base and differ in the binding fields.

Base (common to both formats):

FieldTypeDescription
querystringA chain of CTEs (the clauses are separated by \n line breaks): without the leading WITH and without the trailing SELECT, to splice it into a WITH … SELECT.
finalCteNamestringName of the final CTE of the chain; always CTE_VALID_EVENTS. It is the "handle" of the result: a consumer does SELECT … FROM CTE_VALID_EVENTS.
subjectColumnstring | nullIdentity column exposed by the final CTE. In an event filter it is always null.
  • format: "parameterized":

    FieldTypeDescription
    format"parameterized"Discriminator.
    paramsRecord<string, ScalarValue>Binding of each @pN → its value. ScalarValue = string | number | boolean.
    typesRecord<string, string>BigQuery parameter type of each @pN: STRING, TIMESTAMP, BOOL, FLOAT64, INT64, NUMERIC, BIGNUMERIC.
  • format: "literal" — only the base; no params or types.

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.

subjectColumn is null in every event filter: the final CTE projects events, not subjects, and the catalog does not declare an identity column. The columns the CTE exposes are those of the definition's select.

Example — parameterized (default)

201 response to the example POST above:

{
  "format": "parameterized",
  "query": "CTE_VALID_EVENTS AS (\nSELECT DISTINCT `event_transactions`.`event_id`\nFROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions\nWHERE `event_transactions`.`status` = @p0 AND EXISTS (SELECT 1 FROM UNNEST(`event_transactions`.`items`) AS `items_item`, `reten-prod.tenant_a1b2c3d4.product_properties` AS `product_properties` WHERE `product_properties`.`sku` = `items_item`.`item_id` AND (`product_properties`.`categorization`.`category_name` IN (@p1, @p2)))\n)",
  "finalCteName": "CTE_VALID_EVENTS",
  "subjectColumn": null,
  "params": { "p0": "Facturada", "p1": "Food Service", "p2": "HORECA" },
  "types": { "p0": "STRING", "p1": "STRING", "p2": "STRING" }
}

The query is a single string with \n line breaks. Formatted for readability:

CTE_VALID_EVENTS AS (
SELECT DISTINCT `event_transactions`.`event_id`
FROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions
WHERE `event_transactions`.`status` = @p0
  AND EXISTS (
    SELECT 1
    FROM UNNEST(`event_transactions`.`items`) AS `items_item`,
         `reten-prod.tenant_a1b2c3d4.product_properties` AS `product_properties`
    WHERE `product_properties`.`sku` = `items_item`.`item_id`
      AND (`product_properties`.`categorization`.`category_name` IN (@p1, @p2))
  )
)

Each @pN is bound with params[pN] (its value) and types[pN] (its BigQuery type); without that binding the query is not executable.

How the chain is consumed

The response is not an executable query: it is the chain of CTEs that has to be wrapped. In the snippets below, {query} is replaced by the value of the query field exactly as it comes: it already carries its own CTE_VALID_EVENTS AS ( … ), so the CTE header is not written again.

To run it stand-alone:

WITH {query}
SELECT * FROM CTE_VALID_EVENTS

To join it against another query, {query} is spliced into the caller's own WITH —followed by a comma— and you JOIN CTE_VALID_EVENTS on the event column that the select of the definition projects (for example event_id):

WITH
{query},
mis_metricas AS (
  SELECT t.event_id, t.pricing.final_amount
  FROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS t
)
SELECT SUM(m.final_amount) AS total
FROM mis_metricas AS m
JOIN CTE_VALID_EVENTS AS v ON v.event_id = m.event_id

The name CTE_VALID_EVENTS is stable and does not depend on the filter: it is the finalCteName of every event filter. The columns it exposes, on the other hand, are those of the select of each definition.

Example — literal

The same definition with format: "literal":

{
  "format": "literal",
  "query": "CTE_VALID_EVENTS AS (\nSELECT DISTINCT `event_transactions`.`event_id`\nFROM `reten-prod.tenant_a1b2c3d4.event_transactions` AS event_transactions\nWHERE `event_transactions`.`status` = 'Facturada' AND EXISTS (SELECT 1 FROM UNNEST(`event_transactions`.`items`) AS `items_item`, `reten-prod.tenant_a1b2c3d4.product_properties` AS `product_properties` WHERE `product_properties`.`sku` = `items_item`.`item_id` AND (`product_properties`.`categorization`.`category_name` IN ('Food Service', 'HORECA')))\n)",
  "finalCteName": "CTE_VALID_EVENTS",
  "subjectColumn": null
}

The values are inlined as safe literals (escaped strings in single quotes, TIMESTAMP('…') for instants, NUMERIC '…' for exact decimals); there are no params or types.

Validation Rules

Before compiling, the definition goes through the two layers of validation, and both return 400 Bad Request. Business validation runs on both endpoints: generate-sql validates the body definition, and :id/sql validates the saved filter before compiling it.

The full catalog of rules and their exact messages is in Definition → Validation Rules.

Error Responses

StatusDescription
400Invalid 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.
401Missing or invalid Authorization: Bearer.
403The user does not have the event-criteria:view-sql permission, or the x-tenant-id is not authorized.
404In :id/sql, no event filter with that id exists in the tenant (Event criteria not found), or the tenant does not have a dataset configured.