This guide covers common data operations using @photostructure/sqlite, including prepared statements, parameter binding, transactions, and data type handling.
Prepared statements are the recommended way to execute SQL queries. They provide:
import { DatabaseSync } from "@photostructure/sqlite";
const db = new DatabaseSync("myapp.db");
// Prepare a statement
const stmt = db.prepare("SELECT * FROM users WHERE age > ?");
// Execute it multiple times with different parameters
const adults = stmt.all(18);
const seniors = stmt.all(65);
// Always finalize statements when done (or let them be garbage collected)
stmt.finalize();
db.close();
// .run() - Execute statement, returns info about changes
const insert = db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
const info = insert.run("Alice", 30);
console.log(info.changes); // Number of rows affected
console.log(info.lastInsertRowid); // ID of inserted row
// .get() - Return first row
const select = db.prepare("SELECT * FROM users WHERE id = ?");
const user = select.get(1);
console.log(user); // { id: 1, name: 'Alice', age: 30 }
// .all() - Return all rows as array
const selectAll = db.prepare("SELECT * FROM users WHERE age > ?");
const users = selectAll.all(25);
console.log(users); // Array of user objects
// .iterate() - Return iterator for memory-efficient row processing
const iter = db.prepare("SELECT * FROM users");
for (const user of iter) {
console.log(user);
// Process one row at a time
}
Note: DataView parameter binding is not currently supported. Use Buffer instead for binary data.
const stmt = db.prepare(
"INSERT INTO users (name, age, email) VALUES (?, ?, ?)",
);
stmt.run("Bob", 25, "bob@example.com");
const stmt = db.prepare(
"INSERT INTO users (name, age, email) VALUES ($name, $age, $email)",
);
stmt.run({ $name: "Charlie", $age: 35, $email: "charlie@example.com" });
// Also works with : prefix
const stmt2 = db.prepare(
"INSERT INTO users (name, age, email) VALUES (:name, :age, :email)",
);
stmt2.run({ name: "David", age: 40, email: "david@example.com" });
// Enable anonymous parameters for a specific statement
const stmt = db.prepare("INSERT INTO logs (message) VALUES (?)", {
anonymousParameters: true,
});
// Now you can bind any number of parameters
stmt.run("Error", "Details", "Stack trace"); // All concatenated
SQLite supports several data types, and this library handles JavaScript type conversions automatically:
SQLite Type | JavaScript Type | Notes |
---|---|---|
NULL | null | |
INTEGER | number or bigint | BigInt for values outside safe integer range |
REAL | number | |
TEXT | string | |
BLOB | Buffer | Node.js Buffer objects (DataView not supported) |
const db = new DatabaseSync(":memory:");
// Create table with various types
db.exec(`
CREATE TABLE data_types (
id INTEGER PRIMARY KEY,
count INTEGER,
price REAL,
name TEXT,
data BLOB,
created_at TEXT
)
`);
const insert = db.prepare(`
INSERT INTO data_types (count, price, name, data, created_at)
VALUES (?, ?, ?, ?, ?)
`);
// Insert different types
insert.run(
42, // INTEGER
19.99, // REAL
"Product", // TEXT
Buffer.from("binary data"), // BLOB
new Date().toISOString(), // TEXT (store dates as ISO strings)
);
// Retrieve and check types
const row = db.prepare("SELECT * FROM data_types WHERE id = ?").get(1);
console.log(typeof row.count); // 'number'
console.log(typeof row.price); // 'number'
console.log(typeof row.name); // 'string'
console.log(row.data); // <Buffer ...>
// Large integers automatically returned as BigInt
db.exec("CREATE TABLE big_numbers (value INTEGER)");
const bigInsert = db.prepare("INSERT INTO big_numbers VALUES (?)");
bigInsert.run(9007199254740993n); // Using BigInt literal
const result = db.prepare("SELECT value FROM big_numbers").get();
console.log(result.value); // 9007199254740993n
console.log(typeof result.value); // 'bigint'
Transactions ensure data consistency by grouping multiple operations into a single atomic unit.
const db = new DatabaseSync("bank.db");
try {
db.exec("BEGIN TRANSACTION");
const withdraw = db.prepare(
"UPDATE accounts SET balance = balance - ? WHERE id = ?",
);
const deposit = db.prepare(
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
);
// Transfer $100 from account 1 to account 2
withdraw.run(100, 1);
deposit.run(100, 2);
db.exec("COMMIT");
console.log("Transfer successful");
} catch (error) {
db.exec("ROLLBACK");
console.error("Transfer failed:", error.message);
} finally {
db.close();
}
// Default transaction
db.exec("BEGIN");
// or
db.exec("BEGIN TRANSACTION");
// Deferred transaction (default)
db.exec("BEGIN DEFERRED");
// Immediate transaction (acquires RESERVED lock)
db.exec("BEGIN IMMEDIATE");
// Exclusive transaction (acquires EXCLUSIVE lock)
db.exec("BEGIN EXCLUSIVE");
db.exec("BEGIN");
try {
db.exec('INSERT INTO users (name) VALUES ("Alice")');
// Create savepoint
db.exec("SAVEPOINT sp1");
try {
db.exec('INSERT INTO users (name) VALUES ("Bob")');
// This might fail
db.exec("INSERT INTO users (name) VALUES (NULL)"); // Error if NOT NULL
} catch (error) {
// Rollback to savepoint
db.exec("ROLLBACK TO sp1");
console.log("Rolled back to savepoint");
}
db.exec("COMMIT");
} catch (error) {
db.exec("ROLLBACK");
}
SQLite operations can throw errors for various reasons. This package provides enhanced error information to help with debugging and error recovery.
When an SQLite error occurs, the thrown Error
object includes additional properties:
sqliteCode
(number): The primary SQLite error code (e.g., 14
for SQLITE_CANTOPEN
)sqliteExtendedCode
(number): The extended error code providing more specific information (e.g., 2067
for SQLITE_CONSTRAINT_UNIQUE
)code
(string): The SQLite error constant name (e.g., "SQLITE_CANTOPEN"
, "SQLITE_CONSTRAINT"
)sqliteErrorString
(string): Human-readable description of the error (e.g., "unable to open database file"
)systemErrno
(number): The underlying OS error number for I/O operations. Only present when the error involves file system operations.import { DatabaseSync } from "@photostructure/sqlite";
try {
const db = new DatabaseSync("/nonexistent/path/database.db", {
readOnly: true,
});
} catch (error) {
console.log(error.message); // "Failed to open database: unable to open database file"
console.log(error.sqliteCode); // 14
console.log(error.sqliteExtendedCode); // 14
console.log(error.code); // "SQLITE_CANTOPEN"
console.log(error.sqliteErrorString); // "unable to open database file"
console.log(error.systemErrno); // 2 (ENOENT on Unix)
}
// Handling constraint violations
try {
const stmt = db.prepare("INSERT INTO users (id, email) VALUES (?, ?)");
stmt.run(1, "duplicate@email.com"); // This email already exists
} catch (error) {
if (error.code === "SQLITE_CONSTRAINT_UNIQUE") {
console.log("Email already exists:", error.sqliteErrorString);
}
}
Code | Name | Description |
---|---|---|
1 | SQLITE_ERROR |
Generic error |
5 | SQLITE_BUSY |
Database is locked |
8 | SQLITE_READONLY |
Attempt to write a readonly database |
14 | SQLITE_CANTOPEN |
Unable to open database file |
19 | SQLITE_CONSTRAINT |
Constraint violation |
2067 | Extended: SQLITE_CONSTRAINT_UNIQUE |
UNIQUE constraint failed |
1555 | Extended: SQLITE_CONSTRAINT_PRIMARYKEY |
PRIMARY KEY constraint failed |
// Inserting NULL values
const stmt = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
stmt.run("Alice", null); // email will be NULL
// Checking for NULL in queries
const nullCheck = db.prepare("SELECT * FROM users WHERE email IS NULL");
const usersWithoutEmail = nullCheck.all();
// COALESCE to provide default values
const withDefaults = db.prepare(
'SELECT name, COALESCE(email, "no-email@example.com") as email FROM users',
);
SQLite doesn't have a native date type, so dates are typically stored as TEXT, INTEGER, or REAL.
// Store as ISO string (TEXT)
const insertDate = db.prepare("INSERT INTO events (name, date) VALUES (?, ?)");
insertDate.run("Meeting", new Date().toISOString());
// Store as Unix timestamp (INTEGER)
const insertTimestamp = db.prepare(
"INSERT INTO events (name, timestamp) VALUES (?, ?)",
);
insertTimestamp.run("Meeting", Math.floor(Date.now() / 1000));
// Query and parse dates
const events = db.prepare("SELECT * FROM events").all();
events.forEach((event) => {
if (event.date) {
const date = new Date(event.date);
console.log(`${event.name} at ${date.toLocaleString()}`);
}
});
// Use SQLite date functions
const recent = db
.prepare(
`
SELECT * FROM events
WHERE date > datetime('now', '-7 days')
`,
)
.all();
try {
const stmt = db.prepare("SELECT * FROM nonexistent_table");
stmt.all();
} catch (error) {
console.error("SQLite error:", error.message);
console.error("Error code:", error.code);
console.error("SQLite code:", error.sqliteCode);
console.error("Extended code:", error.sqliteExtendedCode);
}