neverchange
Version:
NeverChange is a database solution for web applications using SQLite WASM and OPFS.
31 lines (28 loc) • 8.19 kB
JavaScript
(function(r,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("@sqlite.org/sqlite-wasm")):typeof define=="function"&&define.amd?define(["exports","@sqlite.org/sqlite-wasm"],c):(r=typeof globalThis<"u"?globalThis:r||self,c(r.neverchange={},r.sqlite3InitModule))})(this,function(r,c){"use strict";var b=Object.defineProperty;var A=(r,c,h)=>c in r?b(r,c,{enumerable:!0,configurable:!0,writable:!0,value:h}):r[c]=h;var m=(r,c,h)=>A(r,typeof c!="symbol"?c+"":c,h);const h={version:0,up:async u=>{await u.execute(`
CREATE TABLE IF NOT EXISTS migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)}},E=u=>{const t=[];let e="",i=!1;for(let s=0;s<u.length;s++){const n=u[s];n==='"'?i&&u[s+1]==='"'?(e+='"',s++):i=!i:n===","&&!i?(t.push(e),e=""):e+=n}return t.push(e),t};let g=0;class T{constructor(t,e={}){m(this,"dbPromise",null);m(this,"dbId",null);m(this,"migrations",[]);m(this,"transactionDepth",0);m(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([h])}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=>{c.sqlite3Worker1Promiser({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(s=>s.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 s="";e&&(s+=`PRAGMA foreign_keys = OFF;
BEGIN TRANSACTION;
`);const n=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'",l=await this.query(n,i?[i]:[]);for(const a of l)if(a.type==="table"){s+=`${a.sql};
`;const o=await this.query(`SELECT * FROM ${a.name}`);for(const d of o){const f=Object.keys(d).join(", "),w=Object.values(d).map(p=>p instanceof Uint8Array?this.escapeBlob(p):p===null?"NULL":typeof p=="string"?`'${p.replace(/'/g,"''")}'`:p).join(", ");s+=`INSERT INTO ${a.name} (${f}) VALUES (${w});
`}s+=`
`}if(!i)try{const a=await this.query("SELECT * FROM sqlite_sequence");if(a.length>0){s+=`DELETE FROM sqlite_sequence;
`;for(const o of a)s+=`INSERT INTO sqlite_sequence VALUES('${o.name}', ${o.seq});
`;s+=`
`}}catch{this.log("sqlite_sequence table does not exist, skipping...")}if(!i)for(const a of l)a.type!=="table"&&(s+=`${a.sql};
`);return e&&(s+=`COMMIT;
`),s}async importDump(t,e={}){const{compatibilityMode:i=!1}=e,s=t.split(";").map(n=>n.trim()).filter(Boolean);i||(await this.execute("PRAGMA foreign_keys=OFF"),await this.execute("BEGIN TRANSACTION"));try{const n=await this.query(`
SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name != 'sqlite_sequence'
`);for(const{type:l,name:a}of n)await this.execute(`DROP ${l} IF EXISTS ${a}`);for(const l of s)l!=="COMMIT"&&await this.execute(l);i||(await this.execute("COMMIT"),await this.execute("PRAGMA foreign_keys = ON"))}catch(n){throw i||(await this.execute("ROLLBACK"),await this.execute("PRAGMA foreign_keys = ON")),n}}async dumpTableToCSV(t,e={}){const i=await this.query(`SELECT * FROM ${t}`),s=Object.keys(i[0]||(await this.query(`PRAGMA table_info(${t})`)).reduce((a,o)=>({...a,[o.name]:""}),{})).map(a=>e.quoteAllFields?`"${a}"`:a).join(",");if(i.length===0)return`${s}\r
`;const n=a=>{const o=(a==null?void 0:a.toString())||"";return e.quoteAllFields||o.includes(",")||o.includes(`
`)||o.includes('"')?`"${o.replace(/"/g,'""')}"`:o},l=i.map(a=>Object.values(a).map(n).join(","));return`${s}\r
${l.join(`\r
`)}\r
`}async importCSVToTable(t,e){const[i,...s]=e.split(/\r?\n/).filter(Boolean),n=i.split(",");for(const l of s){const a=E(l),o=n.map(()=>"?").join(",");await this.execute(`INSERT INTO ${t} (${n.join(",")}) VALUES (${o})`,a)}}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_${g++}`;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}}}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.")}}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}`)}}}r.NeverChangeDB=T,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});