neverchange
Version:
NeverChange is a database solution for web applications using SQLite WASM and OPFS.
313 lines (310 loc) • 11.1 kB
JavaScript
var E = Object.defineProperty;
var d = (l, t, e) => t in l ? E(l, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : l[t] = e;
var u = (l, t, e) => d(l, typeof t != "symbol" ? t + "" : t, e);
import { sqlite3Worker1Promiser as w } from "@sqlite.org/sqlite-wasm";
const g = {
version: 0,
up: async (l) => {
await l.execute(`
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
}
}, f = (l) => {
const t = l.replace(/^\uFEFF/, "");
if (t.length === 0) return [];
const e = [];
let i = [], o = "", r = !1, n = !1;
const s = () => {
i.push(o), n && e.push(i), i = [], o = "", n = !1;
};
for (let a = 0; a < t.length; a++) {
const c = t[a], p = t[a + 1];
if (c === '"') {
n = !0, r && p === '"' ? (o += '"', a++) : r ? r = !1 : o === "" ? r = !0 : o += c;
continue;
}
if (!r && c === ",") {
n = !0, i.push(o), o = "";
continue;
}
if (!r && (c === `
` || c === "\r")) {
c === "\r" && p === `
` && a++, s();
continue;
}
n = !0, o += c;
}
if (r)
throw new Error("CSV parsing error: unclosed quoted field");
return n && s(), e;
};
let T = 0;
class O {
constructor(t, e = {}) {
u(this, "dbPromise", null);
u(this, "dbId", null);
u(this, "migrations", []);
u(this, "transactionDepth", 0);
u(this, "savepointStack", []);
this.dbName = t, this.options = e, this.options.debug = e.debug ?? !1, this.options.isMigrationActive = e.isMigrationActive ?? !0, this.options.isMigrationActive && this.addMigrations([g]);
}
log(...t) {
this.options.debug && console.log(...t);
}
async init() {
if (!this.dbPromise)
try {
this.dbPromise = this.initializeDatabase(), await this.dbPromise, this.options.isMigrationActive && (await this.createMigrationTable(), await this.runMigrations());
} catch (t) {
throw console.error("Failed to initialize database:", t), t;
}
}
async initializeDatabase() {
this.log("Loading and initializing SQLite3 module...");
const t = await this.getPromiser();
this.log("Done initializing. Opening database...");
const e = await this.openDatabase(t);
return this.dbId = e.result.dbId, this.log("Database initialized successfully"), t;
}
async getPromiser() {
return new Promise(
(t) => {
w({
onready: (e) => t(e)
});
}
);
}
async openDatabase(t) {
try {
const e = await t("open", {
filename: `file:${this.dbName}.sqlite3?vfs=opfs`
});
return this.log("OPFS database opened:", e.result.filename), e;
} catch (e) {
console.warn(
"OPFS is not available, falling back to in-memory database:",
e
);
const i = await t("open", { filename: ":memory:" });
return this.log("In-memory database opened"), i;
}
}
async execute(t, e = []) {
try {
return await (await this.getPromiserOrThrow())("exec", { sql: t, bind: e, dbId: this.dbId });
} catch (i) {
throw console.error("Error executing SQL:", i), i;
}
}
async query(t, e = []) {
return (await (await this.getPromiserOrThrow())("exec", {
sql: t,
bind: e,
rowMode: "object",
dbId: this.dbId
})).result.resultRows || [];
}
async close() {
this.dbId && (await (await this.getPromiserOrThrow())("close", { dbId: this.dbId }), this.dbId = null, this.dbPromise = null);
}
addMigrations(t) {
this.migrations.push(...t), this.migrations.sort((e, i) => e.version - i.version);
}
async createMigrationTable() {
await this.execute(`
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
}
async getCurrentVersion() {
var i;
return (await this.query(
"SELECT name FROM sqlite_master WHERE type='table'"
)).some((o) => o.name === "migrations") && ((i = (await this.query(
"SELECT MAX(version) as version FROM migrations"
))[0]) == null ? void 0 : i.version) || 0;
}
async runMigrations() {
const t = await this.getCurrentVersion(), e = this.migrations.filter(
(i) => i.version > t
);
for (const i of e)
this.log(`Running migration to version ${i.version}`), await i.up(this), await this.execute("INSERT INTO migrations (version) VALUES (?)", [
i.version
]), this.log(`Migration to version ${i.version} completed`);
}
async getPromiserOrThrow() {
if (!this.dbPromise)
throw new Error("Database not initialized. Call init() first.");
return this.dbPromise;
}
escapeBlob(t) {
return `X'${Array.from(new Uint8Array(t), (e) => e.toString(16).padStart(2, "0")).join("")}'`;
}
async dumpDatabase(t = {}) {
const { compatibilityMode: e = !1, table: i } = t;
let o = "";
e && (o += `PRAGMA foreign_keys = OFF;
BEGIN TRANSACTION;
`);
const r = i ? "SELECT type, name, sql FROM sqlite_master WHERE type='table' AND name = ?" : "SELECT type, name, sql FROM sqlite_master WHERE sql NOT NULL AND name != 'sqlite_sequence'", n = await this.query(r, i ? [i] : []);
for (const s of n)
if (s.type === "table") {
o += `${s.sql};
`;
const a = await this.query(`SELECT * FROM ${s.name}`);
for (const c of a) {
const p = Object.keys(c).join(", "), m = Object.values(c).map((h) => h instanceof Uint8Array ? this.escapeBlob(h) : h === null ? "NULL" : typeof h == "string" ? `'${h.replace(/'/g, "''")}'` : h).join(", ");
o += `INSERT INTO ${s.name} (${p}) VALUES (${m});
`;
}
o += `
`;
}
if (!i)
try {
const s = await this.query(
"SELECT * FROM sqlite_sequence"
);
if (s.length > 0) {
o += `DELETE FROM sqlite_sequence;
`;
for (const a of s)
o += `INSERT INTO sqlite_sequence VALUES('${a.name}', ${a.seq});
`;
o += `
`;
}
} catch {
this.log("sqlite_sequence table does not exist, skipping...");
}
if (!i)
for (const s of n)
s.type !== "table" && (o += `${s.sql};
`);
return e && (o += `COMMIT;
`), o;
}
async importDump(t, e = {}) {
const { compatibilityMode: i = !1 } = e, o = t.split(";").map((r) => r.trim()).filter(Boolean);
i || (await this.execute("PRAGMA foreign_keys=OFF"), await this.execute("BEGIN TRANSACTION"));
try {
const r = await this.query(`
SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name != 'sqlite_sequence'
`);
for (const { type: n, name: s } of r)
await this.execute(`DROP ${n} IF EXISTS ${s}`);
for (const n of o)
n !== "COMMIT" && await this.execute(n);
i || (await this.execute("COMMIT"), await this.execute("PRAGMA foreign_keys = ON"));
} catch (r) {
throw i || (await this.execute("ROLLBACK"), await this.execute("PRAGMA foreign_keys = ON")), r;
}
}
async dumpTableToCSV(t, e = {}) {
const i = await this.query(`SELECT * FROM ${t}`), o = Object.keys(
i[0] || (await this.query(`PRAGMA table_info(${t})`)).reduce(
(s, a) => ({ ...s, [a.name]: "" }),
{}
)
).map((s) => e.quoteAllFields ? `"${s}"` : s).join(",");
if (i.length === 0)
return `${o}\r
`;
const r = (s) => {
const a = (s == null ? void 0 : s.toString()) || "";
return e.quoteAllFields || a.includes(",") || a.includes(`
`) || a.includes('"') ? `"${a.replace(/"/g, '""')}"` : a;
}, n = i.map(
(s) => Object.values(s).map(r).join(",")
);
return `${o}\r
${n.join(`\r
`)}\r
`;
}
async importCSVToTable(t, e) {
const i = f(e);
if (i.length === 0) return;
const [o, ...r] = i, n = o;
for (let s = 0; s < r.length; s++) {
const a = r[s];
if (a.length !== n.length)
throw new Error(
`CSV row ${s + 2} has ${a.length} fields, but header has ${n.length} columns`
);
const c = n.map(() => "?").join(",");
await this.execute(
`INSERT INTO ${t} (${n.join(",")}) VALUES (${c})`,
a
);
}
}
/**
* execute a transaction.
* - if it's top-level, it will be a top-level transaction.
* - if it's nested, it will be a nested transaction.
*/
async transaction(t) {
if (this.transactionDepth === 0) {
await this.execute("BEGIN TRANSACTION"), this.transactionDepth = 1, this.log("BEGIN TRANSACTION (top-level)");
try {
const e = await t(this);
return await this.execute("COMMIT"), this.log("COMMIT (top-level)"), this.transactionDepth = 0, e;
} catch (e) {
throw await this.execute("ROLLBACK"), this.log("ROLLBACK (top-level)"), this.transactionDepth = 0, e;
}
} else {
this.transactionDepth++;
const e = `sp_${T++}`;
this.savepointStack.push(e), this.log(`BEGIN NESTED TRANSACTION: SAVEPOINT ${e}`), await this.execute(`SAVEPOINT ${e}`);
try {
const i = await t(this);
return this.log(`RELEASE SAVEPOINT ${e}`), await this.execute(`RELEASE SAVEPOINT ${e}`), this.savepointStack.pop(), this.transactionDepth--, i;
} catch (i) {
throw this.log(`ROLLBACK TO ${e}`), await this.execute(`ROLLBACK TO ${e}`), this.savepointStack.pop(), this.transactionDepth--, i;
}
}
}
/**
* explicitly rollback the current transaction.
* - if it's top-level, it will rollback all changes.
* - if it's nested, it will rollback to the last created savepoint.
*/
async rollback() {
if (this.transactionDepth <= 0)
throw new Error("rollback() called but no active transaction exists.");
if (this.transactionDepth === 1)
throw this.log("ROLLBACK (top-level)"), await this.execute("ROLLBACK"), this.transactionDepth = 0, new Error("Transaction rolled back (top-level).");
{
const t = this.savepointStack.pop();
throw this.transactionDepth--, t ? (this.log(`ROLLBACK TO ${t} (nested)`), await this.execute(`ROLLBACK TO ${t}`), new Error(`Transaction rolled back to savepoint ${t}.`)) : new Error("rollback() called but no matching savepoint found.");
}
}
/**
* if you want to commit explicitly, you can call this method.
* (but it will be committed automatically when the transaction(cb) ends.)
*/
async commit() {
if (this.transactionDepth <= 0)
throw new Error("commit() called but no active transaction exists.");
if (this.transactionDepth === 1)
await this.execute("COMMIT"), this.log("COMMIT (top-level)"), this.transactionDepth = 0;
else {
const t = this.savepointStack.pop();
if (this.transactionDepth--, !t)
throw new Error("commit() called but no matching savepoint found.");
this.log(`RELEASE SAVEPOINT ${t} (nested by user)`), await this.execute(`RELEASE SAVEPOINT ${t}`);
}
}
}
export {
O as NeverChangeDB
};