Raw sql template literal
npm install @flowblade/sql-tagFast and lightweight (~700B) sql template tag based on sql-template-tag.









- π‘οΈ Take advantage of template literals to prevent sql injections.
- π€² Facilitate query composition and conditional clauses.
- π¦ Separate actual sql from provided parameters.
- β‘οΈ Minimal performance overhead.
- π Lightweight (less than ~700B)
- βΎοΈοΈ Tested on node 20-24, bun, browser, workers and edge.
- π Available in ESM and CJS formats.
``bash`
yarn add @flowblade/sql-tag
| Helpers | Description | Example |
| --------- | ----------------------------------------- | ---------------------------------------------------- |
| sql.join | Join array values with optional separator | AND id IN (${sql.join(['1', '3'])) |AND ${sql.if(true, () => sql'deleted_at is null')}
| sql.if | Conditionally add a statement | |ORDER BY ${sql.raw('name desc')}
| sql.bulk | Ease bulk inserts | |
| sql.raw | Allow to pass unsafe values in the query. | |${isTrue ? sql'1=1' : sql.empty}
| sql.empty | Helper to represent empty string. | |
`typescript
import { sql } from "@flowblade/sql-tag";
// π User provided parameters
const params = {
country: "BE",
users: ["John", "Doe"],
};
const query = sql<{
// π optionally type the result
id: number;
username: string;
}>
SELECT id, username FROM users
WHERE country = ${params.country} -- π simple param
AND username IN (${sql.join(params.users)}) -- π array param;
// query.sql === "SELECT id, username FROM users WHERE country = ? AND username IN (?, ?)";
// query.values === ['BE', 'John', 'Doe'];
`
`typescript
import { sql } from "@flowblade/sql-tag";
// π User provided parameters
const userIds = [1, 2];
const limit = 10;
const query = sql<{
// π optionally type the result
id: number;
username: string;
}>
SELECT id, username FROM users
WHERE 1=1
-- π alternative 2: with ternary operator and sql.empty
${userIds.length > 0 ? sqlAND id IN (${sql.join(userIds)}) : sql.empty}
-- π alternative 2: with usage of sql.if helper
${sql.if(userIds.length, () => sqlAND id IN (${sql.join(userIds)}))}
LIMIT ${limit};
// query.sql === "SELECT id, username FROM users WHERE 1=1 AND id IN (?, ?) LIMIT ?";
// query.values === [1, 2, 10];
`
You can nest any query into another one.
`typescript
import { sql } from "@flowblade/sql-tag";
const getSqlUserCountByCountries = (minUsers: number) => sql
SELECT
c.name as country_name,
count(u.id) as user_count
FROM country AS c INNER JOIN user u
ON c.id = u.country_id
GROUP BY c.name
HAVING count(u.id) > ${minUsers};
const compression: "zstd" | "snappy" | "gzip" = "zstd";
// Example base on DuckDb COPY statement
// but you can nest into CTE, table aliases, subqueries etc...
const query = sql
COPY
(${getSqlUserCountByCountries(23)})
TO 'usercount_by_countries.parquet'
(FORMAT 'parquet', COMPRESSION ${compression}, ROW_GROUP_SIZE 100000);;
console.log(query.values); // [23, 'zstd']
console.log(query.sql); // "COPY (SELECT...."
`
Ease bulk inserts/merge from multi rows arrays.
`typescript
import { sql } from "@flowblade/sql-tag";
const insert = sql
INSERT INTO product (name, price, stock, status)
VALUES ${sql.bulk([
["Laptop", 999.99, 50, "active"],
["Keyboard", 79.99, 100, "active"],
])}
;
const { text, sql, statement, values } = insert;
insert.text; //=> "INSERT INTO product (name, price, stock, status) VALUES ($1,$2,$3,$4),($5,$6,$7,$8)"
insert.sql; //=> "INSERT INTO product (name, price, stock, status) VALUES (?,?,?,?),(?,?,?,?),(?,?,?,?)"
insert.values; //=> ["Laptop", 999.99, 50, "active", "Keyboard", 79.99, 100, "active"]
// Example running the query with pglite
const result = await db.query(text, values, {});
`
`typescript
import { sql } from "@flowblade/sql-tag";
const products = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: Product ${i},
}));
const limit = 10;
const sqlRaw = sql<{ id: number; name: string }>
-- TRANSACT-SQL
DECLARE @Products NVARCHAR(MAX); -- WARNING LIMIT TO 2GB
SET @Products = ${JSON.stringify(products)};
-- DDL (# prefix is equivalent to CREATE TEMPORATY TABLE on other db)
CREATE TABLE #products (
id INT,
name NVARCHAR(255),
);
-- INSERT
INSERT INTO #products (id, name)
SELECT id, name
FROM OPENJSON(@Products) WITH (
id INT,
name NVARCHAR(255)
);
-- SELECT
SELECT TOP ${sql.raw(String(limit))} id, name
FROM #products
ORDER BY id;;`
This package won't be possible without the great work of Blake Embrey sql-template-tag.
Some notable differences:
- [x] Named export for sql: import { sql } from '@flowblade/sql-tag'.sql
- [x] Possibility to type the result of the query (ie ).sql.if
- [x] Utility functions (join...) are directly available from the sql tag.
- [x] Add helper to conditionally add a statement.
Bundle size is tracked by a size-limit configuration
| Scenario (esm) | Size (compressed) |
| ----------------------------------------- | ----------------: |
| import { sql } from '@flowblade/sql-tag` | ~ 700B |
| Level | CI | Description |
| ------------ | --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node | β
| CI for 20.x, 22.x, 24.x & 25.x. |
| Bun | β
| Tested on latest (1.3.5) |
| Browser | β
| Tested with latest chrome (vitest/playwright) |
| Browserslist | β
| > 95% on 01/2025. Chrome 96+, Firefox 90+, Edge 19+, ios 15+, Safari 15+ and Opera 77+ |
| Edge | β
| Ensured on CI with @vercel/edge-runtime. |
| Cloudflare | β
| Ensured with @cloudflare/vitest-pool-workers (see wrangler.toml |
| Typescript | β
| TS 5.0 + / are-the-type-wrong checks on CI. |
| ES2022 | β
| Dist files checked with es-check |
| Performance | β
| Monitored with codspeed.io |
Contributions are welcome. Have a look to the CONTRIBUTING document.
Sponsor>), coffee>),
or star β All is spent for quality time with loved ones. Thanks ! πβ€οΈ
![]() | |
JetBrains | Embie.be |
MIT Β© SΓ©bastien Vanvelthem and contributors.