sqlite prepare
A tagged template literal styled sqlite query builder.
Why choose this one?
No, you should probably use drizzle or prisma for robustness, typesafty, and ergonomics.
Why do you build this?
For small and CRUD based projects, sometimes I want to use more flexible and faster ways to build APIs. These cases, setting up a schema would be a nuisance.
However writing raw sql is cumbersome, and the mismatching between the questions marks and the parameters is a pain.
(consider: INSERT INTO panel_info (user, login_time, created_at, last_edited, recuring_period, ..., subscriptions) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
... so heart breaking to refactor)
With that being said, I will be more than appreciated and happy if you like this project by a newbie.
Installation
npm install sqlite-prepare
yarn add sqlite-prepare
pnpm add sqlite-prepare
Usage
import { build, prepare } from "sqlite-prepare";
const query = build`
SELECT * FROM users
WHERE age > ${21}
AND status = ${"active"}
`;
const results = await prepare(db, query).all();
const sql = wrapD1(db);
const query = await sql`
SELECT * FROM users
WHERE age > ${21}
AND status = ${"active"}
`.all();
const subquery = build`SELECT id FROM users WHERE age > ${21}`;
const mainQuery = build`SELECT * FROM posts WHERE author_id IN ${subquery}`;
const posts = await prepare(db, mainQuery).all();
import { raw } from "sqlite-prepare";
const rawQuery = build`SELECT * FROM ${raw("users")}`;
const newestEntry = async (table: string) => {
const allowedTables = ["cache", "pages"];
const sql = wrapD1(db);
if (allowedTables.includes(table)) {
const query = await sql`
SELECT * FROM ${raw(table)}
WHERE age > ${21}
AND status = ${"active"}
`.all();
} else {
throw new Error("Illegal operation!");
}
};
const getEntries = async (
table: string,
condition?: { field: string; value: any }
) => {
const allowedTables = ["cache", "pages"];
const sql = wrapD1(db);
if (allowedTables.includes(table)) {
let query = sql`SELECT * FROM ${raw(table)}`;
if (condition) {
query = sql`SELECT * FROM ${raw(table)} WHERE ${raw(condition.field)} = ${
condition.value
}`;
}
return await query.all();
} else {
throw new Error("Illegal operation!");
}
};
Syntax Highlighting
Inline SQL is a very nice vscode extension, enabling inline sqlite syntax highlighting for not only html but also jsx and tsx files.
(There are tons of such extensions. Search at the marketplace if you want to try them out. )
Features
Parsing parameters:
- Array:
[1, 2, 3, 4, 5] => (?, ?, ?, ?, ?) & [1, 2, 3, 4, 5]
- Date: Only parameterize, value unchanged, SQLite parameter binding handles this conversion automatically
- Uint8Array | ArrayBuffer: Only parameterize, value unchanged, SQLite parameter binding handles this conversion automatically
- Normal javascript object: JSON.stringify
SQL Fragments
Break down complex queries into reusable parts:
import { fragment as f } from "sqlite-prepare";
const columns = f`id, name, created_at`;
const condition = f`status = ${"active"} AND type = ${"user"}`;
const query = sql`
SELECT ${columns}
FROM users
WHERE ${condition}
`;
Insert Builder
Type-safe insert operations:
import { insert, into } from "sqlite-prepare";
const query = insert(into("users"), {
name: "John",
age: 30,
});
const query = insert(into("users"), {
created_at: raw("NOW()"),
});
const query = insert(into("users"), [
{ name: "John", age: 30 },
{ name: "Jane", age: 25 },
{ name: "Bob", age: 35 }
]);
const query = insert(into("users"), [
{ name: "John", age: 30 },
{ name: "Jane", age: 25, role: "admin" },
{ name: "Bob", status: "active" }
]);
const query = insert(into("users"), [
{
name: "John",
created_at: raw("datetime('now', '-1 day')"),
metadata: { role: "user", permissions: ["read"] }
},
{
name: "Jane",
created_at: raw("datetime('now')"),
metadata: { role: "admin", permissions: ["read", "write"] }
}
]);
const query = insert(into("users"), [
{
name: "John",
role_id: build`SELECT id FROM roles WHERE name = ${'admin'}`
},
{
name: "Jane",
role_id: build`SELECT id FROM roles WHERE name = ${'user'}`
}
]);
Enhanced Raw SQL
Raw SQL, with supports for parameterization:
import { raw } from "sqlite-prepare";
const condition = raw('age > ? AND status = ?', [18, 'active']);
const condition = raw('age > 18 AND status = 'active'');
const query = sql`SELECT * FROM users WHERE ${condition}`;
SQL Validation
Validate raw SQL queries:
import { validate as v } from "sqlite-prepare";
import { v } from "sqlite-prepare";
const allowed = ["SELECT * FROM users", "SELECT * FROM posts"];
const validated = v("SELECT * FROM users", allowed);
const isSelect = (sql: string) => sql.toLowerCase().startsWith("select");
const validated = v("SELECT * FROM users", isSelect);
String validators can be used againt both string and RawSQL. It only checks the query part for the latter one, the params are ignored.
Validator functions also supports (sqlQuery: RawSQL, parsed: true) => boolean
, if you also want to check the params. If the second parameter is passed (no matter true or false), the validate function will pass the entire RawSQL to it.
Discover more at the test suites and the source code.
Database Compatibility
Now tested and works with:
- Cloudflare D1
- better-sqlite3
- Bun's SQLite API
import Database from "better-sqlite3";
const db = new Database("mydb.sqlite");
const query = prepare(db)`SELECT * FROM users WHERE age > ${18}`;
const results = query.all();
Updates
2025-05-06 v0.0.8
- Added support for batch insert operations
- Handles arrays of objects with same or different structures
- Automatically collects all possible columns across all rows
- Sets NULL for missing columns in each row
- Supports complex values in batch inserts:
- Raw SQL expressions
- SQL fragments
- Subqueries
- JSON objects (automatically stringified)
- Date objects
- Binary data
- Maintains parameterization for SQL injection prevention
- Fully tested with better-sqlite3 integration tests
License
MIT