> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# The SQL tag: a function called without parentheses

> How a tagged template splits SQL from its values, why that is plain JavaScript, what TypeScript adds on top, and what else changed in UQL 0.100.0.

Source: https://uql-orm.dev/blog/sql-tag

Here is a line of UQL 0.100:

```ts
await pool.all`SELECT * FROM "User" WHERE "email" = ${email}`;
```

No parentheses, yet it is a function call. How JavaScript makes that call is what keeps `email` out of the SQL: it is sent as a bound parameter, never spliced into the text. Here is how.

## What a tag receives

A template literal with a function name is a [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates). It is plain JavaScript, there since ES2015, so it runs in every browser, Node and Bun with no build step.

The engine calls the tag with the template cut into pieces: first an array of the literal text, then each `${}` value as an argument of its own. A tag that prints its arguments shows the split:

```ts
function show(texts: TemplateStringsArray, ...values: unknown[]) {
  console.log(texts, values);
}

show`SELECT * FROM "User" WHERE "email" = ${'ana@example.com'} AND "age" > ${18}`;
// ['SELECT * FROM "User" WHERE "email" = ', ' AND "age" > ', '']  ['ana@example.com', 18]
```

There is always one more string than there are values; here the last one is empty.

## From pieces to a query

The text pieces are fixed in your source code. The values arrive at run time, from users, other services or anywhere else. A database driver expects exactly that split: SQL with placeholders, and the parameters beside it.

So this:

```ts
function sql(texts: TemplateStringsArray, ...values: unknown[]) {
  const text = texts.reduce((t, part, i) => `${t}$${i}${part}`);
  return { text, values };
}

sql`SELECT * FROM "User" WHERE "email" = ${email}`;
```

Produces this:

```ts
{ text: 'SELECT * FROM "User" WHERE "email" = $1', values: ['ana@example.com'] }
```

Whatever `email` holds, an injection attempt like `'; DROP TABLE "User"; --` included, is sent as the value of `$1`, never as SQL. Nothing to escape by hand.

In UQL the method is the tag: `all` and `run` take the template directly, so there is no `sql` helper to import and wrap.

## The parentheses trap

Add parentheses and you have a different program:

```ts
await pool.all`SELECT * FROM "User" WHERE "email" = ${email}`;

// @ts-expect-error a plain string, with the value already inside it
await pool.all(`SELECT * FROM "User" WHERE "email" = ${email}`);
```

The second line builds the whole string first, `email` and all, and hands the function the finished text. No library can take the value back out, so in UQL that line does not compile.

## What TypeScript adds

TypeScript checks each `${}` against the tag’s parameter types, so UQL can state what a database can bind: strings, numbers, booleans, `bigint`, `Date`, bytes, `null`, lists of those, and SQL fragments. Two common slips stop at the compiler:

```ts
// @ts-expect-error `status` may be undefined, which binds nothing
await pool.all`SELECT * FROM "User" WHERE "status" = ${status}`;

// @ts-expect-error an object is not a SQL value
await pool.run`UPDATE "User" SET "prefs" = ${prefs} WHERE "id" = ${id}`;

// this one compiles
await pool.run`UPDATE "User" SET "prefs" = ${JSON.stringify(prefs)} WHERE "id" = ${id}`;
```

A tag also takes a type argument, so ``pool.all<{ id: number; email: string }>`...` ``hands back typed rows.

## Fragments

`sql` is the same tag without running anything: it returns a fragment. A fragment inside another template is spliced in, its values still bound:

```ts
import { sql } from 'uql-orm';

const conditions = [sql`"createdAt" > ${since}`];
if (status) conditions.push(sql`"status" = ${status}`);

await pool.all`SELECT * FROM "User" WHERE ${sql.join(conditions, ' AND ')}`;
```

For trusted SQL held in a string, such as a file’s contents, use `sql.text(sql)`. It is the only call that binds nothing, so it is easy to find in a review. A tag binds values, not names: a table or column taken from user input still needs checking against a list you control.

## Also in 0.100.0

### `undefined` in a filter throws

Prisma’s docs warn that `deleteMany({ where: { id: undefined } })` [deletes every user](https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined), because an undefined key is dropped from the filter. UQL used to drop it too; now it throws:

```ts
await pool.deleteMany(User, { $where: { email: maybeEmail } });
// UqlUsageError: $where on 'User' holds undefined at 'email': leave the key out not to filter by it, or name null
```

### Writes that do what the code says

A `transaction()` inside another is a savepoint, so a failing inner step rolls back alone. A to-many update keeps the children the payload lists and removes the rest. Any write can take [`returning`](https://uql-orm.dev/querying/methods.md#returning) and resolve to the rows it wrote, typed by the fields you name.

## Upgrading

`npx uql-codemod` updates your imports and turns `all(sql, [a, b])` into a tag. Anything it cannot convert safely is listed for you. The [upgrade guide](https://uql-orm.dev/upgrade-guide.md) has the rest.

---

---

*[UQL](https://uql-orm.dev/index.md) is a JSON-native TypeScript ORM for Node.js and Bun. Supports PostgreSQL, PGlite, MySQL, MariaDB, MSSQL, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.*
