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

# TINQL

> A fully-featured query language unique to TIN.

export const PlatformAvailability = ({current, vitess, postgres, neki}) => {
  const docsHref = path => {
    if (!path) return path;
    const normalized = path.startsWith('/') ? path : `/${path}`;
    return normalized;
  };
  const labels = {
    vitess: 'Vitess',
    postgres: 'Postgres',
    neki: 'Neki'
  };
  const combinedLabels = {
    both: 'Vitess and Postgres',
    all: 'Vitess, Neki, and Postgres',
    'postgres-neki': 'Postgres and Neki'
  };
  if (combinedLabels[current]) {
    return <div className="not-prose mb-5 flex flex-wrap items-center gap-2" role="group" aria-label="Platform availability">
        <span data-engine="both" data-state="current" aria-current="true" className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[13px] font-semibold leading-tight no-underline data-[engine=vitess]:data-[state=current]:border-[#ffc59b] data-[engine=vitess]:data-[state=current]:bg-[#ffe8d8] data-[engine=vitess]:data-[state=current]:text-[#672002] dark:data-[engine=vitess]:data-[state=current]:border-[#962d00] dark:data-[engine=vitess]:data-[state=current]:bg-[#3c1403] dark:data-[engine=vitess]:data-[state=current]:text-[#ffe8d8] data-[engine=vitess]:data-[state=link]:border-[#ffc59b] data-[engine=vitess]:data-[state=link]:bg-transparent data-[engine=vitess]:data-[state=link]:text-[#b83a05] dark:data-[engine=vitess]:data-[state=link]:border-[#962d00] dark:data-[engine=vitess]:data-[state=link]:bg-transparent dark:data-[engine=vitess]:data-[state=link]:text-[#ffc59b] data-[engine=postgres]:data-[state=current]:border-[#a9dffe] data-[engine=postgres]:data-[state=current]:bg-[#ddf2ff] data-[engine=postgres]:data-[state=current]:text-[#0e3682] dark:data-[engine=postgres]:data-[state=current]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=current]:bg-[#08204e] dark:data-[engine=postgres]:data-[state=current]:text-[#ddf2ff] data-[engine=postgres]:data-[state=link]:border-[#a9dffe] data-[engine=postgres]:data-[state=link]:bg-transparent data-[engine=postgres]:data-[state=link]:text-[#0b6ec5] dark:data-[engine=postgres]:data-[state=link]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=link]:bg-transparent dark:data-[engine=postgres]:data-[state=link]:text-[#73c7f9] data-[engine=neki]:data-[state=current]:border-[#fbca00] data-[engine=neki]:data-[state=current]:bg-[#fbca00] data-[engine=neki]:data-[state=current]:text-[#1a1a1a] dark:data-[engine=neki]:data-[state=current]:border-[#fbca00] dark:data-[engine=neki]:data-[state=current]:bg-[#fbca00] dark:data-[engine=neki]:data-[state=current]:text-[#1a1a1a] data-[engine=neki]:data-[state=link]:border-[#fbca00] data-[engine=neki]:data-[state=link]:bg-transparent data-[engine=neki]:data-[state=link]:text-[#8f7200] dark:data-[engine=neki]:data-[state=link]:border-[#fbca00] dark:data-[engine=neki]:data-[state=link]:bg-transparent dark:data-[engine=neki]:data-[state=link]:text-[#fbca00] data-[engine=both]:data-[state=current]:border-[#d4d4d4] data-[engine=both]:data-[state=current]:bg-[#f0f0f0] data-[engine=both]:data-[state=current]:text-[#3d3d3d] dark:data-[engine=both]:data-[state=current]:border-[#525252] dark:data-[engine=both]:data-[state=current]:bg-[#2a2a2a] dark:data-[engine=both]:data-[state=current]:text-[#e5e5e5]">
          {combinedLabels[current]}
        </span>
      </div>;
  }
  const hasVitess = current === 'vitess' || Boolean(vitess);
  const hasPostgres = current === 'postgres' || Boolean(postgres);
  const hasNeki = current === 'neki' || Boolean(neki);
  const only = [hasVitess, hasPostgres, hasNeki].filter(Boolean).length === 1;
  const engines = [];
  if (current === 'vitess' || current === 'postgres' || current === 'neki') engines.push(current);
  if (hasVitess && current !== 'vitess') engines.push('vitess');
  if (hasNeki && current !== 'neki') engines.push('neki');
  if (hasPostgres && current !== 'postgres') engines.push('postgres');
  return <div className="not-prose mb-5 flex flex-wrap items-center gap-2" role="group" aria-label="Platform availability">
      {engines.map(engine => {
    const isCurrent = current === engine;
    const href = docsHref(engine === 'vitess' ? vitess : engine === 'postgres' ? postgres : neki);
    const label = only ? `${labels[engine]} only` : labels[engine];
    const state = isCurrent || !href ? 'current' : 'link';
    if (isCurrent || !href) {
      return <span key={engine} data-engine={engine} data-state={state} aria-current={isCurrent ? 'true' : undefined} className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[13px] font-semibold leading-tight no-underline data-[engine=vitess]:data-[state=current]:border-[#ffc59b] data-[engine=vitess]:data-[state=current]:bg-[#ffe8d8] data-[engine=vitess]:data-[state=current]:text-[#672002] dark:data-[engine=vitess]:data-[state=current]:border-[#962d00] dark:data-[engine=vitess]:data-[state=current]:bg-[#3c1403] dark:data-[engine=vitess]:data-[state=current]:text-[#ffe8d8] data-[engine=vitess]:data-[state=link]:border-[#ffc59b] data-[engine=vitess]:data-[state=link]:bg-transparent data-[engine=vitess]:data-[state=link]:text-[#b83a05] dark:data-[engine=vitess]:data-[state=link]:border-[#962d00] dark:data-[engine=vitess]:data-[state=link]:bg-transparent dark:data-[engine=vitess]:data-[state=link]:text-[#ffc59b] data-[engine=postgres]:data-[state=current]:border-[#a9dffe] data-[engine=postgres]:data-[state=current]:bg-[#ddf2ff] data-[engine=postgres]:data-[state=current]:text-[#0e3682] dark:data-[engine=postgres]:data-[state=current]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=current]:bg-[#08204e] dark:data-[engine=postgres]:data-[state=current]:text-[#ddf2ff] data-[engine=postgres]:data-[state=link]:border-[#a9dffe] data-[engine=postgres]:data-[state=link]:bg-transparent data-[engine=postgres]:data-[state=link]:text-[#0b6ec5] dark:data-[engine=postgres]:data-[state=link]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=link]:bg-transparent dark:data-[engine=postgres]:data-[state=link]:text-[#73c7f9] data-[engine=neki]:data-[state=current]:border-[#fbca00] data-[engine=neki]:data-[state=current]:bg-[#fbca00] data-[engine=neki]:data-[state=current]:text-[#1a1a1a] dark:data-[engine=neki]:data-[state=current]:border-[#fbca00] dark:data-[engine=neki]:data-[state=current]:bg-[#fbca00] dark:data-[engine=neki]:data-[state=current]:text-[#1a1a1a] data-[engine=neki]:data-[state=link]:border-[#fbca00] data-[engine=neki]:data-[state=link]:bg-transparent data-[engine=neki]:data-[state=link]:text-[#8f7200] dark:data-[engine=neki]:data-[state=link]:border-[#fbca00] dark:data-[engine=neki]:data-[state=link]:bg-transparent dark:data-[engine=neki]:data-[state=link]:text-[#fbca00] data-[engine=both]:data-[state=current]:border-[#d4d4d4] data-[engine=both]:data-[state=current]:bg-[#f0f0f0] data-[engine=both]:data-[state=current]:text-[#3d3d3d] dark:data-[engine=both]:data-[state=current]:border-[#525252] dark:data-[engine=both]:data-[state=current]:bg-[#2a2a2a] dark:data-[engine=both]:data-[state=current]:text-[#e5e5e5]">
              {label}
            </span>;
    }
    return <a key={engine} href={href} data-engine={engine} data-state={state} title={`View ${labels[engine]} documentation`} className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border px-2.5 py-1 text-[13px] font-semibold leading-tight no-underline data-[engine=vitess]:data-[state=current]:border-[#ffc59b] data-[engine=vitess]:data-[state=current]:bg-[#ffe8d8] data-[engine=vitess]:data-[state=current]:text-[#672002] dark:data-[engine=vitess]:data-[state=current]:border-[#962d00] dark:data-[engine=vitess]:data-[state=current]:bg-[#3c1403] dark:data-[engine=vitess]:data-[state=current]:text-[#ffe8d8] data-[engine=vitess]:data-[state=link]:border-[#ffc59b] data-[engine=vitess]:data-[state=link]:bg-transparent data-[engine=vitess]:data-[state=link]:text-[#b83a05] dark:data-[engine=vitess]:data-[state=link]:border-[#962d00] dark:data-[engine=vitess]:data-[state=link]:bg-transparent dark:data-[engine=vitess]:data-[state=link]:text-[#ffc59b] data-[engine=postgres]:data-[state=current]:border-[#a9dffe] data-[engine=postgres]:data-[state=current]:bg-[#ddf2ff] data-[engine=postgres]:data-[state=current]:text-[#0e3682] dark:data-[engine=postgres]:data-[state=current]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=current]:bg-[#08204e] dark:data-[engine=postgres]:data-[state=current]:text-[#ddf2ff] data-[engine=postgres]:data-[state=link]:border-[#a9dffe] data-[engine=postgres]:data-[state=link]:bg-transparent data-[engine=postgres]:data-[state=link]:text-[#0b6ec5] dark:data-[engine=postgres]:data-[state=link]:border-[#144eb6] dark:data-[engine=postgres]:data-[state=link]:bg-transparent dark:data-[engine=postgres]:data-[state=link]:text-[#73c7f9] data-[engine=neki]:data-[state=current]:border-[#fbca00] data-[engine=neki]:data-[state=current]:bg-[#fbca00] data-[engine=neki]:data-[state=current]:text-[#1a1a1a] dark:data-[engine=neki]:data-[state=current]:border-[#fbca00] dark:data-[engine=neki]:data-[state=current]:bg-[#fbca00] dark:data-[engine=neki]:data-[state=current]:text-[#1a1a1a] data-[engine=neki]:data-[state=link]:border-[#fbca00] data-[engine=neki]:data-[state=link]:bg-transparent data-[engine=neki]:data-[state=link]:text-[#8f7200] dark:data-[engine=neki]:data-[state=link]:border-[#fbca00] dark:data-[engine=neki]:data-[state=link]:bg-transparent dark:data-[engine=neki]:data-[state=link]:text-[#fbca00] data-[engine=both]:data-[state=current]:border-[#d4d4d4] data-[engine=both]:data-[state=current]:bg-[#f0f0f0] data-[engine=both]:data-[state=current]:text-[#3d3d3d] dark:data-[engine=both]:data-[state=current]:border-[#525252] dark:data-[engine=both]:data-[state=current]:bg-[#2a2a2a] dark:data-[engine=both]:data-[state=current]:text-[#e5e5e5]">
            {label}
            <svg aria-hidden="true" width="12" height="12" viewBox="0 0 12 12" fill="none" className="shrink-0">
              <path d="M2.5 6h7M6.5 3l3 3-3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </a>;
  })}
    </div>;
};

<PlatformAvailability current="postgres" />

TIN adds [the `==>` operator](/docs/postgres/search/reference/operator). The query on the right-hand side of it is **TINQL**.

```sql theme={null}
SELECT id, body
FROM posts
WHERE body ==> 'apple AND "fuji apple"';
```

**Rules**

* Keywords must be **UPPER CASE** (`AND`, `OR`, `THEN`). Lowercase is a search term (`and` matches the word “and”).
* Matching **folds case and accents** under the default tokenizer (`Jalapeño` and `jalapeno` are the same term). Override with `case_folding` / `accent_folding` on the index.
* Inputs that analyze to **no tokens** (`''`, whitespace, bare punctuation) match nothing. Explicit empty syntax (`""`, `[]`) is a parse error.
* Query boost factors (`^N`) must be in `[0.0..10000.0]`. The system rejects out-of-range values at parse time.
* Token positions are **0-based**. Prefer `IN FIRST N WORDS` / `IN LAST N WORDS` over raw `IN WORDS` ranges when you mean “the first/last N words.”
* Juxtaposition defaults to AND: `apple grape` acts as `apple AND grape`.
* Exclude terms with `AND NOT`: `apple AND NOT peel`.
* Quote reserved words used as terms:  `"AND"`, `"THEN"`.
* A hyphen usually splits a term: `wi-fi` is searched as `"wi fi"`.
* Fuzzy match (`apple~2`) works on one word. `wi-fi~2` errors because `wi-fi` is two words.

## Terms

| Form                | Meaning                                                   | Example                                                    |
| ------------------- | --------------------------------------------------------- | ---------------------------------------------------------- |
| bare term           | Single dictionary term (case + accent folded by default)  | `apple` (same as `Apple`), `jalapeno` (same as `Jalapeño`) |
| `*`                 | Match-all documents (standalone only)                     | `*`                                                        |
| `*` / `?` wildcards | `*` = 0+ chars, `?` = 1 char                              | `appl*`, `p?ach`, `*house`                                 |
| `term~N`            | Fuzzy: edit distance N, stable prefix 1                   | `apple~2`                                                  |
| `term~P:N`          | Fuzzy: stable prefix P, distance N                        | `apple~0:2`                                                |
| `A TO B`            | Inclusive term-dictionary range                           | `aardvark TO cat`                                          |
| `* TO B` / `A TO *` | Open range bound                                          | `* TO cat`                                                 |
| `MATCHES regex`     | Full-term regex (to unescaped whitespace); **not** folded | `MATCHES peach.*`                                          |
| `CONTAINS term`     | Same as bare term (optional)                              | `CONTAINS apple`                                           |
| `expr^N`            | Boost (`N` ∈ `[0.0..10000.0]`)                            | `apple^2`, `"x"^1.5`                                       |

Allowed in terms: anything except whitespace and `( ) [ ] " ~ ^`. Escape literal `*` / `?` with `\`. Space in a regex: `MATCHES foo\ bar`.

Because `MATCHES` is not folded, write patterns against the normalized dictionary form (usually lowercase, accents folded): `MATCHES apple.*`, not `MATCHES Apple.*`.

## Phrases

| Form          | Meaning                                    | Example                  |
| ------------- | ------------------------------------------ | ------------------------ |
| `"a b c"`     | Adjacent ordered words                     | `"fuji apple"`           |
| `"a _ c"`     | `_` = one any-word gap                     | `"big _ wolf"`           |
| `"a [b c] d"` | Per-position alternatives                  | `"big [bad large] wolf"` |
| `"a b"~N`     | Up to N extra word gaps (phrase tolerance) | `"fuji apple"~2`         |

Phrase escapes: `\"` `\\` `\_` `\[` `\]`. Empty `""` is invalid (parse error). Inputs that analyze to no tokens match nothing.

## Boolean operators

| Operator      | Meaning                  | Example              |
| ------------- | ------------------------ | -------------------- |
| `A AND B`     | Both match               | `juicy AND apple`    |
| `A OR B`      | Either matches           | `apple OR grape`     |
| `A AND NOT B` | A matches and B does not | `apple AND NOT peel` |
| `A B`         | Implicit AND             | `apple grape`        |

`NOT` never stands alone. It only appears in `AND NOT`, `NOT ENCLOSES`, `NOT ENCLOSED BY`, and `NOT OVERLAPPING`.

## Alternatives

| Form                 | Meaning                                      | Example                     |
| -------------------- | -------------------------------------------- | --------------------------- |
| `[a b c]`            | OR of whitespace-separated alts (not commas) | `[mango plum pear]`         |
| `AT LEAST N OF […]`  | At least N alts match                        | `AT LEAST 2 OF [a b c d]`   |
| `AT LEAST N% OF […]` | At least N% of alts match                    | `AT LEAST 50% OF [a b c d]` |
| `ALL OF […]`         | Every alt matches                            | `ALL OF [a b c]`            |

Alternatives may hold expressions: `["big bad" house NEAR/2 brick]`.

## Proximity

`/N` is required on `THEN` and `NEAR`. N is the maximum number of extra words allowed between the operands, and `0` means adjacent.

| Operator           | Meaning              | Example                          |
| ------------------ | -------------------- | -------------------------------- |
| `A THEN/N B`       | Ordered proximity    | `fuji THEN/0 apple`              |
| `A NEAR/N B`       | Either order         | `peach NEAR/5 blossom`           |
| `(A … B) WITHIN N` | Match span width ≤ N | `(citrus NEAR/5 melon) WITHIN 6` |

## Span relations

Every TINQL expression produces **spans** (position ranges). Relation operators compare two span sets.

| Operator              | Keeps                                     | Example                                                |
| --------------------- | ----------------------------------------- | ------------------------------------------------------ |
| `A ENCLOSES B`        | Spans of A that contain a span of B       | `(security NEAR/10 threat) ENCLOSES critical`          |
| `A NOT ENCLOSES B`    | Spans of A that contain no span of B      | `* NOT ENCLOSES spam`                                  |
| `A ENCLOSED BY B`     | Spans of A that fall inside a span of B   | `critical ENCLOSED BY (security NEAR/10 threat)`       |
| `A NOT ENCLOSED BY B` | Spans of A not inside any span of B       | `price NOT ENCLOSED BY (disclaimer NEAR/20 terms)`     |
| `A OVERLAPPING B`     | Spans of A that share a position with B   | `(apple NEAR/5 fuji) OVERLAPPING (kiwi NEAR/5 citrus)` |
| `A NOT OVERLAPPING B` | Spans of A that share no positions with B | `title NOT OVERLAPPING disclaimer`                     |
| `A BEFORE B`          | Spans of A that start before B            | `abstract BEFORE conclusion`                           |
| `A AFTER B`           | Spans of A that start after B             | `price AFTER discount`                                 |

`ENCLOSES` and `ENCLOSED BY` test the same relationship but return different spans: `ENCLOSES` returns the outer span and `ENCLOSED BY` returns the inner one.

## Positional filters

Token positions are **0-based** (the first token is position `0`). Use `IN FIRST` / `IN LAST` when you care about word counts, and `IN WORDS` when you need an explicit position window.

| Form                 | Meaning                                             | Example                      |
| -------------------- | --------------------------------------------------- | ---------------------------- |
| `A IN FIRST N WORDS` | Match in the first N tokens (positions `0` … `N-1`) | `apple IN FIRST 100 WORDS`   |
| `A IN FIRST N%`      | Match in first N% of the document                   | `apple IN FIRST 25%`         |
| `A IN LAST N WORDS`  | Match in the last N tokens                          | `apple IN LAST 50 WORDS`     |
| `A IN LAST N%`       | Match in last N% of the document                    | `apple IN LAST 25%`          |
| `A IN MIDDLE N%`     | Match in middle N% of the document                  | `apple IN MIDDLE 50%`        |
| `A IN WORDS X TO Y`  | Match in inclusive 0-based position range `[X, Y]`  | `apple IN WORDS 500 TO 1000` |

## Precedence (loose → tight)

| Level | Operators                                           |
| ----- | --------------------------------------------------- |
| 1     | `OR`                                                |
| 2     | `AND` (explicit and implicit)                       |
| 3     | `AND NOT`                                           |
| 4     | Positional filters (`IN …`)                         |
| 5     | Relation operators (`ENCLOSES`, `BEFORE`, …)        |
| 6     | Proximity (`THEN/N`, `NEAR/N`)                      |
| 7     | `WITHIN`                                            |
| 8     | Boost (`^N`)                                        |
| 9     | Primary (`apple`, `"phrase"`, `[a b]`, `MATCHES …`) |

Operators at the same level are left-associative. Use parentheses to override precedence.

`A OR B THEN/5 C AND D` → `A OR ((B THEN/5 C) AND D)`.

Postfix modifiers compose on the primary: `apple~2^3`, `"big bad"~2^1.5`.

## Keyword index

All keywords are case-sensitive **UPPER CASE**.

| Category         | Keywords                                                       |
| ---------------- | -------------------------------------------------------------- |
| Boolean          | `AND`, `OR`, `NOT` (only in compounds)                         |
| Proximity        | `THEN`, `NEAR`, `WITHIN`                                       |
| Relations        | `ENCLOSES`, `ENCLOSED`, `BY`, `OVERLAPPING`, `BEFORE`, `AFTER` |
| Range / position | `TO`, `IN`, `FIRST`, `LAST`, `MIDDLE`, `WORDS`                 |
| Terms            | `CONTAINS`, `MATCHES`                                          |
| Alternatives     | `AT`, `LEAST`, `OF`, `ALL`                                     |

`IN` is special only before `FIRST` / `LAST` / `MIDDLE` / `WORDS`. `AT` only before `LEAST`. `ALL` only before `OF`. `ENCLOSED` / `BY` only together.

## Special characters

| Character | Meaning                                                  |
| --------- | -------------------------------------------------------- |
| `"…"`     | Phrase                                                   |
| `(…)`     | Grouping                                                 |
| `[…]`     | Alternatives                                             |
| `~N`      | Fuzzy (on term) or phrase word-gap tolerance (after `"`) |
| `~P:N`    | Fuzzy with explicit prefix length                        |
| `^N`      | Boost                                                    |
| `*`       | Wildcard, open range bound, or standalone match-all      |
| `?`       | Single-character wildcard                                |
| `_`       | One-word gap inside a phrase                             |
| `\`       | Escape                                                   |

## Need help?

Get help from [the PlanetScale Support team](https://planetscale.com/contact?initial=support), or join our [Discord community](https://pscale.link/community) to see how others are using PlanetScale.
