> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# TQL Language Reference

> The ontology query language for building reusable, parameterized SQL templates

TQL is the query language used to write ontology queries in TextQL. It is a typed SQL templating language that lets you define reusable, parameterized queries with a small expression layer on top.

Use TQL when you want to expose a stable query surface — one that accepts typed parameters, enforces business logic, and always produces consistent SQL.

<Note>
  **Parameter descriptions are shown in the UI.** When you add `--` comments above a param declaration, they appear as help text in the ontology query panel. Use them to document expected formats, example values, and allowed filter keys.
</Note>

## Two File Shapes

TQL files come in two forms.

### Plain SQL Template

Use this when the template is essentially SQL with a few direct parameters.

```tql theme={null}
params {
  -- Country code. Examples: "US", "CA", "BR"
  country: String = "US"

  -- Inclusive lower bound in ISO 8601 format.
  created_after: Timestamp?
}

SELECT *
FROM visits
WHERE country = ${country}
  AND created_at >= ${created_after}
```

In a plain body, `${...}` interpolates a param name directly into SQL.

### Expression Body

Use this when you need branching, reusable fragments, `matchSet`, or semantic-view logic.

```tql theme={null}
params {
  include_region: Bool = false
}

let
  select_expr = if include_region then sql"region, COUNT(*)" else sql"COUNT(*)"
  group_clause = if include_region then sql"GROUP BY region" else sql""
in sql''
  SELECT ${select_expr}
  FROM orders
  ${group_clause}
''
```

An expression body must evaluate to a `SqlFragment` using `sql"..."` or `sql'' ... ''`.

## Params

Params are declared in a `params { ... }` block — one per line, no commas.

```tql theme={null}
params {
  -- Country code.
  country: String = "US"

  -- Inclusive lower bound in ISO 8601 timestamp format.
  created_after: Timestamp?

  -- Metrics to include.
  metrics: Set<"revenue" | "order_count">

  -- Optional filters.
  filters: List<FilterInput> = []
}
```

### Supported Types

| TQL Type      | JSON Shape       | Notes                                                 |
| ------------- | ---------------- | ----------------------------------------------------- |
| `Int`         | integer          | Non-integer numbers are rejected                      |
| `Float`       | number           | Integers also accepted                                |
| `String`      | string           |                                                       |
| `Bool`        | boolean          |                                                       |
| `Date`        | string           | Document the expected format in `--` comments         |
| `Timestamp`   | string           | Same note as `Date`                                   |
| `Set<"...">`  | array of strings | Values validated against allowed labels               |
| `List<T>`     | array            | Elements validated recursively                        |
| `FilterInput` | object           | Used inside `List<FilterInput>` for dynamic filtering |

### Nullability and Defaults

* `?` marks a nullable param. Omitting it resolves to `null`.
* Non-nullable params without a default are required.
* Defaults are supported for scalars and empty lists/sets.

```tql theme={null}
params {
  limit: Int = 100
  include_test: Bool = false
  country: String = "US"
  dimensions: Set<"buyer" | "seller"> = []
  filters: List<FilterInput> = []
}
```

### Param Descriptions

`--` comments directly above a param declaration are exposed by `inspect` and shown in the UI as help text.

```tql theme={null}
params {
  -- Filter by country code. Examples: "US", "CA", "BR"
  country: String = "US"

  -- Filters to apply. Allowed keys: buyer_name (equals, like), created_at (gte, lte, between)
  filters: List<FilterInput> = []
}
```

## Expressions

### `let` Bindings

```tql theme={null}
let
  left = sql"a"
  right = sql"b"
  cols = concatSep ", " [left, right]
in sql"SELECT ${cols}"
```

Bindings are evaluated in order. Duplicate names are rejected.

### Conditionals

```tql theme={null}
if isEmpty select_dims
then select_metrics
else sql"${select_dims}, ${select_metrics}"
```

### Records and Field Access

```tql theme={null}
let
  buyer = { expr = sql"b.name", join = sql"JOIN buyers b ON t.buyer_id = b.id" }
in sql"SELECT ${buyer.expr}"
```

## SQL Fragments

Use `sql"..."` for short fragments and `sql'' ... ''` for multiline SQL.

```tql theme={null}
sql''
  SELECT ${select_expr}
  FROM orders o
  ${where_clause}
''
```

**Critical rule: never quote interpolations yourself.**

```tql theme={null}
-- Correct
WHERE country = ${country}

-- Wrong — TQL rejects this
WHERE country = '${country}'
```

**Nullable params interpolate as `NULL`**, not as an omitted clause. Use `if` for optional predicates:

```tql theme={null}
let
  where_clause =
    if created_after == null
    then sql""
    else sql"WHERE created_at >= ${created_after}"
```

**List interpolation already adds parentheses:**

```tql theme={null}
-- Correct
WHERE id IN ${ids}

-- Wrong — double parentheses
WHERE id IN (${ids})
```

## Builtins

### `matchSet`

The core tool for set-driven SQL structure. Maps each label in a `Set<"...">` param to a SQL fragment or record.

```tql theme={null}
metric_frags = matchSet metrics {
  "revenue" -> sql"SUM(t.revenue) AS revenue"
  "order_count" -> sql"COUNT(*) AS order_count"
}
```

Output order follows the authored arm order, not the caller's order. Repeated values are deduped.

### `filterWhere`

Lowers user-supplied `FilterInput` values against an authored filter registry.

```tql theme={null}
filterables = [
  { key = "buyer_name", expr = sql"buyer_u.name", ops = ["equals", "like"] }
, { key = "created_at", expr = sql"t.created_at", ops = ["gte", "lte", "between"] }
]
where_clause = wrap "WHERE " "" (filterWhere filterables filters)
```

Supported operators include: `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte`, `like`, `starts_with`, `ends_with`, `between`, `in`, `not_in`, `is_null`, `is_not_null`.

### `concatSep`

Concatenates a list of `SqlFragment` values with a separator. Empty fragments are skipped.

```tql theme={null}
select_dims = concatSep ", " (map (\d -> d.expr) dim_entries)
```

### `wrap`

Adds a prefix and suffix to a fragment, but only if the fragment is non-empty.

```tql theme={null}
where_clause = wrap "WHERE " "" predicates
group_clause = wrap "GROUP BY " "" group_cols
```

### `isEmpty`

Returns `true` for empty fragments, strings, sets, lists, and `null`.

### `map`, `any`, `contains`

```tql theme={null}
map (\d -> d.expr) dim_entries          -- apply function to each item
any (map (\d -> d == "seller") dims)    -- true if any element is true
contains dimensions "buyer"             -- true if set/list contains value
```

## Semantic-View Pattern

The recommended pattern for reusable query surfaces combines `metrics`, `dimensions`, and `filters` params with `matchSet` and `filterWhere`.

```tql theme={null}
params {
  metrics: Set<"revenue" | "gmv" | "order_count">
  dimensions: Set<"buyer" | "seller" | "category"> = []
  filters: List<FilterInput> = []
}

let
  metric_frags = matchSet metrics {
    "revenue" -> sql"SUM(t.revenue) AS revenue"
    "gmv" -> sql"SUM(t.gmv) AS gmv"
    "order_count" -> sql"COUNT(*) AS order_count"
  }
  dim_entries = matchSet dimensions {
    "buyer" -> { expr = sql"buyer_u.name", join = sql"JOIN users buyer_u ON t.buyer_user_id = buyer_u.id" }
    "seller" -> { expr = sql"seller_u.name", join = sql"JOIN users seller_u ON t.seller_user_id = seller_u.id" }
    "category" -> { expr = sql"p.category", join = sql"JOIN products p ON t.product_id = p.id" }
  }
  filterables = [
    { key = "buyer_name", expr = sql"buyer_u.name", ops = ["equals", "like"] }
  , { key = "created_at", expr = sql"t.created_at", ops = ["gte", "lte", "between"] }
  ]
  joins = concatSep " " (map (\d -> d.join) dim_entries)
  select_dims = concatSep ", " (map (\d -> d.expr) dim_entries)
  select_metrics = concatSep ", " metric_frags
  select_expr = if isEmpty select_dims then select_metrics else sql"${select_dims}, ${select_metrics}"
  group_clause = wrap "GROUP BY " "" select_dims
  where_clause = wrap "WHERE " "" (filterWhere filterables filters)
in sql''
  SELECT ${select_expr}
  FROM transactions t
  ${joins}
  ${where_clause}
  ${group_clause}
''
```

This pattern gives callers a compact, stable API while keeping all SQL structure in authored code.

## Common Gotchas

<AccordionGroup>
  <Accordion title="`=` is not equality">
    Use `=` for param defaults, let bindings, and record fields. Use `==` / `!=` for expression equality comparisons.
  </Accordion>

  <Accordion title="Optional filters need explicit branching">
    Nullable params interpolate as `NULL` — they do not remove SQL clauses automatically. Use `if` or `isEmpty` for optional predicates.
  </Accordion>

  <Accordion title="Sets cannot be interpolated directly">
    `SELECT ${metrics}` fails. Use `matchSet` or `contains` to work with set values.
  </Accordion>

  <Accordion title="`concatSep` and `wrap` only accept trusted literals">
    The separator and prefix/suffix must be authored string literals, not params.
  </Accordion>

  <Accordion title="Plain-body interpolation is limited">
    In a plain SQL body, `${...}` only interpolates a param name. For computed fragments like `${select_expr}`, switch to expression-style TQL with `sql"..."`.
  </Accordion>
</AccordionGroup>

## Authoring Checklist

Before saving a new `.tql` file:

* Params are in `params { ... }`, one per line, no commas
* String defaults use double quotes
* Equality tests use `==` / `!=`
* Optional clauses use `if` or `isEmpty`
* Interpolations are not wrapped in SQL quotes
* List interpolations are not wrapped in extra parentheses
* Semantic-view metrics have stable `AS ...` aliases
* `filters` comments document allowed keys and operators
