rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
288 lines (271 loc) • 7.69 kB
text/typescript
import {
BulkWriteRow,
RxDocumentData,
PROMISE_RESOLVE_VOID,
promiseWait,
errorToPlainJson,
MaybePromise
} from '../../index.ts';
import type {
SQLResultRow,
SQLiteBasics,
SQLiteDatabaseClass,
SQLiteQueryWithParams
} from './sqlite-types.ts';
export const NON_IMPLEMENTED_OPERATOR_QUERY_BATCH_SIZE = 50;
export type DatabaseState = {
database: Promise<SQLiteDatabaseClass>;
openConnections: number;
sqliteBasics: SQLiteBasics<SQLiteDatabaseClass>;
}
export const DATABASE_STATE_BY_NAME: Map<string, DatabaseState> = new Map();
export const RX_STORAGE_NAME_SQLITE = 'sqlite';
/**
* @link https://www.sqlite.org/inmemorydb.html
*/
export const SQLITE_IN_MEMORY_DB_NAME = ':memory:';
export function getDatabaseConnection(
sqliteBasics: SQLiteBasics<any>,
databaseName: string
): Promise<SQLiteDatabaseClass> {
let state = DATABASE_STATE_BY_NAME.get(databaseName);
if (!state) {
state = {
database: sqliteBasics.open(databaseName),
sqliteBasics,
openConnections: 1
};
DATABASE_STATE_BY_NAME.set(databaseName, state);
} else {
if (state.sqliteBasics !== sqliteBasics && databaseName !== SQLITE_IN_MEMORY_DB_NAME) {
throw new Error('opened db with different creator method ' + databaseName + ' ' + state.sqliteBasics.debugId + ' ' + sqliteBasics.debugId);
}
state.openConnections = state.openConnections + 1;
}
return state.database;
}
export function closeDatabaseConnection(
databaseName: string,
sqliteBasics: SQLiteBasics<any>
): MaybePromise<void> {
const state = DATABASE_STATE_BY_NAME.get(databaseName);
if (state) {
state.openConnections = state.openConnections - 1;
if (state.openConnections === 0) {
DATABASE_STATE_BY_NAME.delete(databaseName);
return state.database.then(db => sqliteBasics.close(db));
}
}
}
export function getDataFromResultRow(row: SQLResultRow): string {
if (!row) {
return row;
}
if (Array.isArray(row)) {
if (row[4]) {
return row[4];
} else {
return row[0];
}
} else {
return row.data;
}
}
export function getSQLiteInsertSQL<RxDocType>(
collectionName: string,
primaryPath: keyof RxDocType,
docData: RxDocumentData<RxDocType>
): SQLiteQueryWithParams {
// language=SQL
const query = `
INSERT INTO "${collectionName}" (
id,
revision,
deleted,
lastWriteTime,
data
) VALUES (
?,
?,
?,
?,
?
);
`;
const params = [
docData[primaryPath] as string,
docData._rev,
docData._deleted ? 1 : 0,
docData._meta.lwt,
JSON.stringify(docData)
];
return {
query,
params,
context: {
method: 'getSQLiteInsertSQL',
data: {
collectionName,
primaryPath
}
}
};
}
export function getSQLiteUpdateSQL<RxDocType>(
tableName: string,
primaryPath: keyof RxDocType,
writeRow: BulkWriteRow<RxDocType>
): SQLiteQueryWithParams {
const docData = writeRow.document;
// language=SQL
const query = `
UPDATE "${tableName}"
SET
revision = ?,
deleted = ?,
lastWriteTime = ?,
data = json(?)
WHERE
id = ?
`;
const params = [
docData._rev,
docData._deleted ? 1 : 0,
docData._meta.lwt,
JSON.stringify(docData),
docData[primaryPath] as string,
];
return {
query,
params,
context: {
method: 'getSQLiteUpdateSQL',
data: {
tableName,
primaryPath
}
}
};
};
export const TX_QUEUE_BY_DATABASE: WeakMap<SQLiteDatabaseClass, Promise<void>> = new WeakMap();
export function sqliteTransaction(
database: SQLiteDatabaseClass,
sqliteBasics: SQLiteBasics<any>,
handler: () => Promise<'COMMIT' | 'ROLLBACK'>,
/**
* Context will be logged
* if the commit does error.
*/
context?: any
) {
let queue = TX_QUEUE_BY_DATABASE.get(database);
if (!queue) {
queue = PROMISE_RESOLVE_VOID;
}
queue = queue.then(async () => {
await openSqliteTransaction(database, sqliteBasics);
const handlerResult = await handler();
await finishSqliteTransaction(database, sqliteBasics, handlerResult, context);
});
TX_QUEUE_BY_DATABASE.set(database, queue);
return queue;
}
/**
* TODO instead of doing a while loop, we should find a way to listen when the
* other transaction is committed.
*/
export async function openSqliteTransaction(
database: SQLiteDatabaseClass,
sqliteBasics: SQLiteBasics<any>
) {
let openedTransaction = false;
while (!openedTransaction) {
try {
await sqliteBasics.run(
database,
{
query: 'BEGIN;',
params: [],
context: {
method: 'openSqliteTransaction',
data: ''
}
}
);
openedTransaction = true;
} catch (err: any) {
console.log('open transaction error (will retry):');
const errorAsJson = errorToPlainJson(err);
console.log(errorAsJson);
console.dir(err);
if (
err.message && (
err.message.includes('Database is closed') ||
err.message.includes('API misuse')
)
) {
throw err;
}
// wait one tick to not fully block the cpu on errors.
await promiseWait(0);
}
}
return;
}
export function finishSqliteTransaction(
database: SQLiteDatabaseClass,
sqliteBasics: SQLiteBasics<any>,
mode: 'COMMIT' | 'ROLLBACK',
/**
* Context will be logged
* if the commit does error.
*/
context?: any
) {
return sqliteBasics.run(
database,
{
query: mode + ';',
params: [],
context: {
method: 'finishSqliteTransaction',
data: mode
}
}
).catch(err => {
if (context) {
console.error('cannot close transaction (mode: ' + mode + ')');
console.log(JSON.stringify(context, null, 4));
}
throw err;
});
}
export const PARAM_KEY = '?';
export function ensureParamsCountIsCorrect(queryWithParams: SQLiteQueryWithParams) {
const paramsCount = queryWithParams.params.length;
const paramKeyCount = queryWithParams.query.split(PARAM_KEY).length - 1;
if (paramsCount !== paramKeyCount) {
throw new Error('ensureParamsCountIsCorrect() wrong param count: ' + JSON.stringify(queryWithParams));
}
}
/**
* SQLite itself does not know about boolean types
* and uses integers instead.
* So some libraries need to bind integers and fail on booleans.
* @link https://stackoverflow.com/a/2452569/3443137
* This method transforms all boolean params to the
* correct int representation.
*/
export function boolParamsToInt(params: any[]): any[] {
return params.map(p => {
if (typeof p === 'boolean') {
if (p) {
return 1;
} else {
return 0;
}
} else {
return p;
}
});
}