Skip to main content

SQL Tagged Template Literal

The sql tagged template literal provides a clean, safe way to execute SQL queries against Cloudflare's embedded SQLite storage. It gives you the readable ${value} interpolation syntax of template literals, but with automatic parameter binding that prevents SQL injection.

Basic Usage

import { LumenizeDO } from '@lumenize/mesh';

class ProductDO extends LumenizeDO<Env> {
// Initialize schema (runs once, safely wrapped in blockConcurrencyWhile)
async onStart() {
this.svc.sql`
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER DEFAULT 0
)
`;
}

addProduct(id: string, name: string, price: number, stock: number = 0) {
this.svc.sql`
INSERT INTO products (id, name, price, stock)
VALUES (${id}, ${name}, ${price}, ${stock})
`;
return { id, name, price, stock };
}

getProduct(id: string) {
const rows = this.svc.sql`SELECT * FROM products WHERE id = ${id}`;
return rows[0];
}
}

Template Substitution Expressions

Template Substitution Expressions (e.g. ${id}) are automatically bound and SQL-injection safe. The template literal tag:

  1. Safely binds all interpolated values as query parameters
  2. Prevents SQL injection attacks
  3. Handles strings, numbers, and other primitive types automatically

Query Results

The sql function returns an array of result rows. Each row is a plain JavaScript object with column names as keys:

  getLowStockProducts(threshold: number = 10) {
return this.svc.sql`
SELECT id, name, stock FROM products
WHERE stock < ${threshold}
ORDER BY stock ASC
`;
}

Example return value:

[
{ id: 'prod-1', name: 'Widget', stock: 5 },
{ id: 'prod-2', name: 'Gadget', stock: 8 }
]

When to Use the Native SQL API

Any SQL query you can run with this.ctx.storage.sql.exec(), you can run with this.svc.sql—JOINs, aggregations, updates, deletes, etc. However, this.svc.sql returns a simple array, so you lose access to cursor features like streaming, rowsRead/rowsWritten, raw(), etc. So:

Use this.svc.sql for every-day queries with relatively small return sets:

  • Readable ${value} interpolation instead of ? placeholders
  • Automatic parameter binding prevents SQL injection
  • Returns results as an array for easy manipulation

Use this.ctx.storage.sql.exec() directly when you need:

  • Streaming/cursors - process rows without loading all into memory
  • Large result sets - pagination with LIMIT/OFFSET
  • Metadata - access to rowsRead, rowsWritten, etc.
  • Raw mode - returns arrays instead of objects (no repeated column names)
const cursor = this.ctx.storage.sql.exec(
'SELECT * FROM logs WHERE timestamp > ? LIMIT ? OFFSET ?',
cutoffDate, pageSize, offset
);
for (const row of cursor) {
// Process rows one at a time
}
console.log(`Rows read: ${cursor.rowsRead}`);

Implementation Details

This utility uses a tagged template literal—a function that receives the template's string parts and interpolated values separately and can return whatever it wants:

// this.svc.sql`SELECT * FROM users WHERE id = ${id}`
// The tag function receives:
// strings: ['SELECT * FROM users WHERE id = ', '']
// values: [id]
// And transforms them into:
const query = strings.join('?'); // 'SELECT * FROM users WHERE id = ?'
return [...this.ctx.storage.sql.exec(query, ...values)];