UNPKG

rxdb

Version:

A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/

657 lines (614 loc) 21.8 kB
import { ensureNotFalsy, PROMISE_RESOLVE_VOID, getFromMapOrCreate, randomToken, promiseWait } from '../../index.ts'; import type { Sqlite3Type, SQLiteBasics, SQLiteDatabaseClass, SQLiteQueryWithParams } from './sqlite-types.ts'; import { boolParamsToInt } from './sqlite-helpers.ts'; const BASICS_BY_SQLITE_LIB = new WeakMap(); export function getSQLiteBasicsNode( sqlite3: Sqlite3Type ): SQLiteBasics<SQLiteDatabaseClass> { let basics: SQLiteBasics<SQLiteDatabaseClass> = BASICS_BY_SQLITE_LIB.get(sqlite3); if (!basics) { basics = { open: (name: string) => Promise.resolve(new sqlite3.Database(name)), async run( db: SQLiteDatabaseClass, queryWithParams: SQLiteQueryWithParams ) { if (!Array.isArray(queryWithParams.params)) { console.dir(queryWithParams); throw new Error('no params array given for query: ' + queryWithParams.query); } await execSqlSQLiteNode( db, queryWithParams, 'run' ); }, async all( db: SQLiteDatabaseClass, queryWithParams: SQLiteQueryWithParams ) { const result = await execSqlSQLiteNode( db, queryWithParams, 'all' ); return result; }, async setPragma(db, key, value) { return await execSqlSQLiteNode( db, { query: 'PRAGMA ' + key + ' = ' + value, params: [], context: { method: 'setPragma', data: { key, value } } }, 'run' ); }, async close(db: SQLiteDatabaseClass) { return await closeSQLiteDatabaseNode(db); }, journalMode: 'WAL2' }; BASICS_BY_SQLITE_LIB.set(sqlite3, basics); } return basics; } /** * Uses the native sqlite that comes sshipped with node version 22+ * @link https://nodejs.org/api/sqlite.html */ export function getSQLiteBasicsNodeNative( // Pass the `sqlite.DatabaseSync` method here constructor: any ): SQLiteBasics<any> { return { open: async (name: string) => { const db = new constructor(name); return db; }, all: async (db: any, queryWithParams: SQLiteQueryWithParams) => { await promiseWait(0); // TODO we should not need this const prepared = db.prepare(queryWithParams.query); const result = await prepared.all(...mapNodeNativeParams(queryWithParams.params)); return result; }, run: async (db: any, queryWithParams: SQLiteQueryWithParams) => { const prepared = db.prepare(queryWithParams.query); await prepared.run(...mapNodeNativeParams(queryWithParams.params)); }, setPragma: async (db, key, value) => { await db.exec(`pragma ${key} = ${value};`); }, close: async (db: any) => { return db.close(); }, journalMode: 'WAL', }; }; /** * For unknown reason we cannot bind boolean values * and have to map them to one and zero. * TODO create an issue at Node.js */ export function mapNodeNativeParams(params: SQLiteQueryWithParams['params']): SQLiteQueryWithParams['params'] { return params.map(param => { if (typeof param === 'boolean') { if (param) { return 1; } else { return 0; } } else { return param; } }); } /** * Promisified version of db.run() */ export function execSqlSQLiteNode( database: SQLiteDatabaseClass, queryWithParams: SQLiteQueryWithParams, operator: 'run' | 'all' ): any { const debug = false; let resolved = false; return new Promise((res, rej) => { if (debug) { console.log('# execSqlSQLiteNode() ' + queryWithParams.query); } database[operator]( queryWithParams.query, queryWithParams.params, ((err: any, result: any) => { if (resolved) { throw new Error('callback called multiple times ' + queryWithParams.query); } resolved = true; if (err) { if (debug) { console.log('---- ERROR RUNNING SQL:'); console.log(queryWithParams.query); console.dir(queryWithParams.params); console.dir(err); console.log('----'); } rej(err); } else { if (debug) { console.log('execSql() result: ' + database.eventNames()); console.log(queryWithParams.query); console.dir(result); console.log('execSql() result:'); console.log(queryWithParams.query); console.dir(queryWithParams.params); console.log('execSql() result -------------------------'); } res(result); } }) ); }); } export function closeSQLiteDatabaseNode( database: SQLiteDatabaseClass ): Promise<void> { return new Promise((res, rej) => { let resolved = false; database.close((err: any) => { if (resolved) { throw new Error('close() callback called multiple times'); } resolved = true; if ( err && !err.message.includes('Database is closed') ) { rej(err); } else { res(); } }); }); } type SQLiteCapacitorDatabase = any; type SQLiteConnection = any; const BASICS_BY_SQLITE_LIB_CAPACITOR: WeakMap<SQLiteConnection, SQLiteBasics<SQLiteCapacitorDatabase>> = new WeakMap(); const CAPACITOR_CONNECTION_BY_NAME = new Map(); /** * In capacitor it is not allowed to reopen an already * open database connection. So we have to queue the open-close * calls so that they do not run in parallel and we do not open&close * database connections at the same time. */ let capacitorOpenCloseQueue = PROMISE_RESOLVE_VOID; export function getSQLiteBasicsCapacitor( sqlite: SQLiteConnection, capacitorCore: any ): SQLiteBasics<SQLiteCapacitorDatabase> { const basics = getFromMapOrCreate<SQLiteConnection, SQLiteBasics<SQLiteCapacitorDatabase>>( BASICS_BY_SQLITE_LIB_CAPACITOR, sqlite, () => { const innerBasics: SQLiteBasics<SQLiteCapacitorDatabase> = { open(dbName: string) { capacitorOpenCloseQueue = capacitorOpenCloseQueue.then(async () => { const db = await getFromMapOrCreate( CAPACITOR_CONNECTION_BY_NAME, dbName, () => sqlite.createConnection(dbName, false, 'no-encryption', 1) ); await db.open(); return db; }); return capacitorOpenCloseQueue; }, async run( db: SQLiteCapacitorDatabase, queryWithParams: SQLiteQueryWithParams ) { await db.run( queryWithParams.query, queryWithParams.params, false ); }, async all( db: SQLiteCapacitorDatabase, queryWithParams: SQLiteQueryWithParams ) { const result: any = await db.query( queryWithParams.query, queryWithParams.params ); return ensureNotFalsy(result.values); }, setPragma(db, key, value) { return db.execute('PRAGMA ' + key + ' = ' + value, false); }, close(db: SQLiteCapacitorDatabase) { capacitorOpenCloseQueue = capacitorOpenCloseQueue.then(() => { return db.close(); }); return capacitorOpenCloseQueue; }, /** * On android, there is already WAL mode set. * So we do not have to set it by our own. * @link https://github.com/capacitor-community/sqlite/issues/258#issuecomment-1102966087 */ journalMode: capacitorCore.getPlatform() === 'android' ? '' : 'WAL' }; return innerBasics; } ); return basics; } type SQLiteQuickDatabase = any; type SQLiteQuickConnection = any; export const EMPTY_FUNCTION = () => { }; export function getSQLiteBasicsQuickSQLite( openDB: any ): SQLiteBasics<SQLiteQuickDatabase> { return { open: async (name: string) => { return await openDB( { name } ); }, all: async (db: SQLiteQuickConnection, queryWithParams: SQLiteQueryWithParams) => { const result = await db.executeAsync( queryWithParams.query, queryWithParams.params ); return result.rows._array; }, run: (db: SQLiteQuickConnection, queryWithParams: SQLiteQueryWithParams) => { return db.executeAsync( queryWithParams.query, queryWithParams.params ); }, setPragma(db, key, value) { return db.executeAsync( 'PRAGMA ' + key + ' = ' + value, [] ); }, close: async (db: SQLiteQuickConnection) => { return await db.close( EMPTY_FUNCTION, EMPTY_FUNCTION, ); }, journalMode: '', }; } /** * @deprecated Use getSQLiteBasicsExpoSQLiteAsync() instead */ export function getSQLiteBasicsExpoSQLite( openDB: any, options?: any, directory?: any ): SQLiteBasics<any> { return { open: async (name: string,) => { return await openDB(name, options, directory); }, all: (db: any, queryWithParams: SQLiteQueryWithParams) => { const result = new Promise<any>((resolve, reject) => { db.exec( [{ sql: queryWithParams.query, args: queryWithParams.params }], false, (err: any, res: any) => { if (err) { return reject(err); } if (Array.isArray(res)) { const queryResult = res[0]; // there is only one query if (Object.prototype.hasOwnProperty.call(queryResult, 'rows')) { return resolve(queryResult.rows); } return reject(queryResult.error); } return reject(new Error(`getSQLiteBasicsExpoSQLite.all() response is not an array: ${res}`)); } ); }); return result; }, run: (db: any, queryWithParams: SQLiteQueryWithParams) => { return new Promise<any>((resolve, reject) => { db.exec([{ sql: queryWithParams.query, args: queryWithParams.params }], false, (err: any, res: any) => { if (err) { return reject(err); } if (Array.isArray(res) && res[0] && res[0].error) { return reject(res); } else { resolve(res); }; }); }); }, setPragma(db, key, value) { return new Promise<any>((resolve, reject) => { db.exec([{ sql: `pragma ${key} = ${value};`, args: [] }], false, (err: any, res: any) => { if (err) { return reject(err); } if (Array.isArray(res) && res[0] && res[0].error) { return reject(res); } else { resolve(res); }; }); }); }, close: async (db: any) => { return await db.closeAsync(); }, journalMode: '', }; }; /** * @link https://docs.expo.dev/versions/latest/sdk/sqlite/ */ export function getSQLiteBasicsExpoSQLiteAsync( openDB: any, options?: any, directory?: any ): SQLiteBasics<any> { return { open: async (name: string) => { return await openDB(name, options, directory); }, all: async (db: any, queryWithParams: SQLiteQueryWithParams) => { const result = await db.getAllAsync( queryWithParams.query, queryWithParams.params ); return result as any; }, run: async (db: any, queryWithParams: SQLiteQueryWithParams) => { const ret = await db.runAsync( queryWithParams.query, queryWithParams.params ); return ret as any; }, async setPragma(db, key, value) { await db.execAsync('PRAGMA ' + key + ' = ' + value); }, close: async (db: any) => { return await db.closeAsync(); }, journalMode: '', }; }; /** * Build to be compatible with packages * that use the websql npm package like: * @link https://www.npmjs.com/package/react-native-sqlite-2 * @link https://www.npmjs.com/package/websql * Use like: * import SQLite from 'react-native-sqlite-2'; * getRxStorageSQLite({ * sqliteBasics: getSQLiteBasicsWebSQL(SQLite.openDatabase) * }); * */ export function getSQLiteBasicsWebSQL( openDB: any, ): SQLiteBasics<any> { return { open: async (name: string) => { const webSQLDatabase = await openDB(name, '1.0', '', 1); return ensureNotFalsy(webSQLDatabase._db); }, all: async (db: any, queryWithParams: SQLiteQueryWithParams) => { const rawResult = await webSQLExecuteQuery(db, queryWithParams); const rows = Array.from(rawResult.rows); return rows as any; }, run: async (db: any, queryWithParams: SQLiteQueryWithParams) => { await webSQLExecuteQuery(db, queryWithParams); }, setPragma: async (db, key, value) => { await webSQLExecuteQuery(db, { query: `pragma ${key} = ${value};`, params: [], context: { method: 'setPragma', data: { key, value } } }).catch(err => { /** * WebSQL in the browser does not allow us to set any pragma * so we have to catch the error. * @link https://stackoverflow.com/a/10298712/3443137 */ if (err.message.includes('23 not authorized')) { return; } throw err; }); }, close: async (db: any) => { /** * The WebSQL API itself has no close() method. * But some libraries have different custom close methods. */ if (typeof db.closeAsync === 'function') { return await db.closeAsync(); } if (typeof db.close === 'function') { return await db.close(); } }, journalMode: '', }; }; export function webSQLExecuteQuery( db: any, queryWithParams: SQLiteQueryWithParams ): Promise<any> { return new Promise<any>((resolve, reject) => { db.exec( [{ sql: queryWithParams.query, args: queryWithParams.params }], false, (err: any, res: any) => { if (err) { return reject(err); } if (Array.isArray(res) && res[0] && res[0].error) { return reject(res[0].error); } else { return resolve(res[0]); }; } ); }); } /** * Build to be compatible with packages * that use SQLite compiled to webassembly: * @link https://github.com/rhashimoto/wa-sqlite * Use like: * ```ts * import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; * const sqliteWasm = await sqlite3InitModule(); * getSQLiteBasicsWasm({ dbConstructor: sqliteWasm.oo1.DB }); * ``` * */ /** * TODO the wa-sqlite module has problems * when running prepared statements with params * in parallel. So we de-parrallel the runs here. * This is bad for performance and should be fixed at the * wa-sqlite repo. */ let runQueueWasmSQLite: Promise<any> = PROMISE_RESOLVE_VOID; export type WasmSqliteDb = { nr: number; name: string; }; export function getSQLiteBasicsWasm( sqlite3: any, ): SQLiteBasics<WasmSqliteDb> { const debugId = randomToken(5); console.log('getSQLiteBasicsWasm() debugId: ' + debugId); return { debugId, open: (name: string) => { const newQueue = runQueueWasmSQLite.then(async () => { const dbNr = await sqlite3.open_v2(name); return { nr: dbNr, name }; }); runQueueWasmSQLite = newQueue.catch(() => { }); return newQueue as any; }, all: (db, queryWithParams: SQLiteQueryWithParams) => { const newQueue = runQueueWasmSQLite.then(async () => { const result = await sqlite3.execWithParams( db.nr, queryWithParams.query, boolParamsToInt(queryWithParams.params) ); return result.rows; }); runQueueWasmSQLite = newQueue.catch(() => { }); return newQueue as any; }, run: (db, queryWithParams: SQLiteQueryWithParams) => { const newQueue = runQueueWasmSQLite.then(async () => { await sqlite3.run(db.nr, queryWithParams.query, queryWithParams.params); // return new Promise(async (res) => { // console.log('run start! ' + queryWithParams.query); // const runResult = await sqlite3.run(db.nr, queryWithParams, (a1: any, a2: any) => { // console.log('run result ccallback:'); // console.log(JSON.stringify({ a1, a2 })); // res(); // }); // console.log('runResult:'); // console.log(JSON.stringify(runResult)); // }); }); runQueueWasmSQLite = newQueue.catch(() => {}); return newQueue as any; }, setPragma: (db, key, value) => { const newQueue = runQueueWasmSQLite.then(async () => { await sqlite3.exec(db.nr, `pragma ${key} = ${value};`); }); runQueueWasmSQLite = newQueue.catch(() => {}); return newQueue as any; }, close: (db) => { const newQueue = runQueueWasmSQLite.then(async () => { await sqlite3.close(db.nr); }); runQueueWasmSQLite = newQueue.catch(() => {}); return newQueue as any; }, journalMode: "WAL", }; } export function getSQLiteBasicsTauri(sqlite3: Sqlite3Type): SQLiteBasics<any> { let basics: SQLiteBasics<any> = BASICS_BY_SQLITE_LIB.get(sqlite3); if (!basics) { basics = { async open(name: string) { return await sqlite3.load(`sqlite:${name}.db`); }, async run( db: SQLiteDatabaseClass, queryWithParams: SQLiteQueryWithParams, ) { await db.execute(queryWithParams.query, queryWithParams.params); }, async all( db: SQLiteDatabaseClass, queryWithParams: SQLiteQueryWithParams, ) { return await db.select( queryWithParams.query, queryWithParams.params, ); }, async setPragma( db: SQLiteDatabaseClass, key: string, value: string, ) { await db.execute(`PRAGMA ${key} = ${value};`); }, async close(db: SQLiteDatabaseClass) { await db.close(); }, journalMode: 'WAL2', }; BASICS_BY_SQLITE_LIB.set(sqlite3, basics); } return basics; }