rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
486 lines (439 loc) • 15.6 kB
text/typescript
import {
RxJsonSchema,
RxStorageInstanceCreationParams,
RxStorageInstance,
getPrimaryFieldOfPrimaryKey,
EventBulk,
RxStorageChangeEvent,
RxDocumentData,
BulkWriteRow,
RxStorageBulkWriteResponse,
RxStorageQueryResult,
categorizeBulkWriteRows,
ensureNotFalsy,
StringKeys,
addRxStorageMultiInstanceSupport,
RxStorageDefaultCheckpoint,
CategorizeBulkWriteRowsOutput,
RxStorageCountResult,
promiseWait,
getQueryMatcher,
PreparedQuery
} from '../../index.ts';
import { BehaviorSubject, Observable, Subject, filter, firstValueFrom } from 'rxjs';
import type { RxStorageSQLiteTrial } from './index.ts';
import {
closeDatabaseConnection,
ensureParamsCountIsCorrect,
getDatabaseConnection,
getSQLiteUpdateSQL,
RX_STORAGE_NAME_SQLITE,
sqliteTransaction,
getDataFromResultRow,
getSQLiteInsertSQL,
TX_QUEUE_BY_DATABASE
} from './sqlite-helpers.ts';
import type {
SQLiteBasics,
SQLiteInstanceCreationOptions,
SQLiteInternals,
SQLiteQueryWithParams,
SQLiteStorageSettings
} from './sqlite-types.ts';
import { getSortComparator } from '../../rx-query-helper.ts';
import { newRxError } from '../../rx-error.ts';
let instanceId = 0;
export class RxStorageInstanceSQLite<RxDocType> implements RxStorageInstance<
RxDocType,
SQLiteInternals,
SQLiteInstanceCreationOptions,
RxStorageDefaultCheckpoint
> {
public readonly primaryPath: StringKeys<RxDocType>;
private changes$: Subject<EventBulk<RxStorageChangeEvent<RxDocumentData<RxDocType>>, RxStorageDefaultCheckpoint>> = new Subject();
public readonly instanceId = instanceId++;
public closed?: Promise<void>;
public sqliteBasics: SQLiteBasics<any>;
public readonly openWriteCount$ = new BehaviorSubject(0);
private opCount = 0;
constructor(
public readonly storage: RxStorageSQLiteTrial,
public readonly databaseName: string,
public readonly collectionName: string,
public readonly schema: Readonly<RxJsonSchema<RxDocumentData<RxDocType>>>,
public readonly internals: SQLiteInternals,
public readonly options: Readonly<SQLiteInstanceCreationOptions>,
public readonly settings: SQLiteStorageSettings,
public readonly tableName: string,
public readonly devMode: boolean
) {
this.sqliteBasics = storage.settings.sqliteBasics;
this.primaryPath = getPrimaryFieldOfPrimaryKey(this.schema.primaryKey) as any;
}
run(
db: any,
queryWithParams: SQLiteQueryWithParams
) {
if (this.devMode) {
ensureParamsCountIsCorrect(queryWithParams);
}
return this.sqliteBasics.run(db, queryWithParams);
}
all(
db: any,
queryWithParams: SQLiteQueryWithParams
) {
if (this.devMode) {
ensureParamsCountIsCorrect(queryWithParams);
}
this.opCount = this.opCount + 1;
if (this.opCount > 110) {
throw newRxError('SQL3');
}
return this.sqliteBasics.all(db, queryWithParams);
}
/**
* @link https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-insertions-971aff98eef2
*/
async bulkWrite(
documentWrites: BulkWriteRow<RxDocType>[],
context: string
): Promise<RxStorageBulkWriteResponse<RxDocType>> {
this.openWriteCount$.next(this.openWriteCount$.getValue() + 1);
const database = await this.internals.databasePromise;
const ret: RxStorageBulkWriteResponse<RxDocType> = {
error: []
};
const writePromises: Promise<any>[] = [];
let categorized: CategorizeBulkWriteRowsOutput<RxDocType> = {} as any;
await sqliteTransaction(
database,
this.sqliteBasics,
async () => {
if (this.closed) {
this.openWriteCount$.next(this.openWriteCount$.getValue() - 1);
throw new Error('SQLite.bulkWrite(' + context + ') already closed ' + this.tableName + ' context: ' + context);
}
const result = await this.all(
database,
{
query: `SELECT data FROM "${this.tableName}"`,
params: [],
context: {
method: 'bulkWrite',
data: documentWrites
}
}
);
const docsInDb: Map<RxDocumentData<RxDocType>[StringKeys<RxDocType>], RxDocumentData<RxDocType>> = new Map();
result.forEach(docSQLResult => {
const doc = JSON.parse(getDataFromResultRow(docSQLResult));
const id = doc[this.primaryPath];
docsInDb.set(id, doc);
});
categorized = categorizeBulkWriteRows(
this,
this.primaryPath,
docsInDb,
documentWrites,
context
);
ret.error = categorized.errors;
if ((result.length + categorized.bulkInsertDocs.length) > 300) {
throw newRxError('SQL2');
}
categorized.bulkInsertDocs.forEach(row => {
const insertQuery = getSQLiteInsertSQL(
this.tableName,
this.primaryPath as any,
row.document
);
writePromises.push(
this.all(
database,
{
query: insertQuery.query,
params: insertQuery.params,
context: {
method: 'bulkWrite',
data: categorized
}
}
)
);
});
categorized.bulkUpdateDocs.forEach(row => {
const updateQuery = getSQLiteUpdateSQL<RxDocType>(
this.tableName,
this.primaryPath,
row
);
writePromises.push(
this.run(
database,
updateQuery
)
);
});
await Promise.all(writePromises);
// close transaction
if (this.closed) {
this.openWriteCount$.next(this.openWriteCount$.getValue() - 1);
return 'ROLLBACK';
} else {
this.openWriteCount$.next(this.openWriteCount$.getValue() - 1);
return 'COMMIT';
}
},
{
databaseName: this.databaseName,
collectionName: this.collectionName
}
);
if (categorized && categorized.eventBulk.events.length > 0) {
const lastState = ensureNotFalsy(categorized.newestRow).document;
categorized.eventBulk.checkpoint = {
id: lastState[this.primaryPath],
lwt: lastState._meta.lwt
};
this.changes$.next(categorized.eventBulk);
}
return ret;
}
async query(
originalPreparedQuery: PreparedQuery<RxDocType>
): Promise<RxStorageQueryResult<RxDocType>> {
const database = await this.internals.databasePromise;
let result: RxDocumentData<RxDocType>[] = [];
const query = originalPreparedQuery.query;
const skip = query.skip ? query.skip : 0;
const limit = query.limit ? query.limit : Infinity;
const skipPlusLimit = skip + limit;
const queryMatcher = getQueryMatcher(
this.schema,
query as any
);
const subResult = await this.all(
database,
{
query: 'SELECT data FROM "' + this.tableName + '"',
params: [],
context: {
method: 'query',
data: originalPreparedQuery
}
}
);
subResult.forEach(row => {
const docData = JSON.parse(getDataFromResultRow(row));
if (queryMatcher(docData)) {
result.push(docData);
}
});
const sortComparator = getSortComparator(this.schema, query as any);
result = result.sort(sortComparator);
result = result.slice(skip, skipPlusLimit);
return {
documents: result
};
}
async count(
originalPreparedQuery: PreparedQuery<RxDocType>
): Promise<RxStorageCountResult> {
const results = await this.query(originalPreparedQuery);
return {
count: results.documents.length,
mode: 'fast'
};
}
async findDocumentsById(
ids: string[],
withDeleted: boolean
): Promise<RxDocumentData<RxDocType>[]> {
const database = await this.internals.databasePromise;
if (this.closed) {
throw new Error('SQLite.findDocumentsById() already closed ' + this.tableName + ' context: ' + context);
}
const result = await this.all(
database,
{
query: `SELECT data FROM "${this.tableName}"`,
params: [],
context: {
method: 'findDocumentsById',
data: ids
}
}
);
const ret: RxDocumentData<RxDocType>[] = [];
for (let i = 0; i < result.length; ++i) {
const resultRow = result[i];
const doc: RxDocumentData<RxDocType> = JSON.parse(getDataFromResultRow(resultRow));
if (
ids.includes((doc as any)[this.primaryPath]) &&
(
withDeleted || !doc._deleted
)
) {
ret.push(doc);
}
}
return ret;
}
changeStream(): Observable<EventBulk<RxStorageChangeEvent<RxDocumentData<RxDocType>>, RxStorageDefaultCheckpoint>> {
return this.changes$.asObservable();
}
async cleanup(minimumDeletedTime: number): Promise<boolean> {
await promiseWait(0);
await promiseWait(0);
const database = await this.internals.databasePromise;
/**
* Purge deleted documents
*/
const minTimestamp = new Date().getTime() - minimumDeletedTime;
await this.all(
database,
{
query: `
DELETE FROM
"${this.tableName}"
WHERE
deleted = 1
AND
lastWriteTime < ?
`,
params: [
minTimestamp
],
context: {
method: 'cleanup',
data: minimumDeletedTime
}
}
);
return true;
}
async getAttachmentData(_documentId: string, _attachmentId: string): Promise<string> {
throw newRxError('SQL1');
}
async remove(): Promise<void> {
if (this.closed) {
throw new Error('closed already');
}
const database = await this.internals.databasePromise;
const promises = [
this.run(
database,
{
query: `DROP TABLE IF EXISTS "${this.tableName}"`,
params: [],
context: {
method: 'remove',
data: this.tableName
}
}
)
];
await Promise.all(promises);
return this.close();
}
async close(): Promise<void> {
const queue = TX_QUEUE_BY_DATABASE.get(await this.internals.databasePromise);
if (queue) {
await queue;
}
if (this.closed) {
return this.closed;
}
this.closed = (async () => {
await firstValueFrom(this.openWriteCount$.pipe(filter(v => v === 0)));
const database = await this.internals.databasePromise;
/**
* First get a transaction
* to ensure currently running operations
* are finished
*/
await sqliteTransaction(
database,
this.sqliteBasics,
() => {
return Promise.resolve('COMMIT');
}
).catch(() => { });
this.changes$.complete();
await closeDatabaseConnection(
this.databaseName,
this.storage.settings.sqliteBasics
);
})();
return this.closed;
}
}
export async function createSQLiteTrialStorageInstance<RxDocType>(
storage: RxStorageSQLiteTrial,
params: RxStorageInstanceCreationParams<RxDocType, SQLiteInstanceCreationOptions>,
settings: SQLiteStorageSettings
): Promise<RxStorageInstanceSQLite<RxDocType>> {
const sqliteBasics = settings.sqliteBasics;
const tableName = params.collectionName + '-' + params.schema.version;
if (params.schema.attachments) {
throw newRxError('SQL1');
}
const internals: Partial<SQLiteInternals> = {};
const useDatabaseName = (settings.databaseNamePrefix ? settings.databaseNamePrefix : '') + '_trial_' + params.databaseName;
internals.databasePromise = getDatabaseConnection(
storage.settings.sqliteBasics,
useDatabaseName
).then(async (database) => {
await sqliteTransaction(
database,
sqliteBasics,
async () => {
const tableQuery = `
CREATE TABLE IF NOT EXISTS "${tableName}"(
id TEXT NOT NULL PRIMARY KEY UNIQUE,
revision TEXT,
deleted BOOLEAN NOT NULL CHECK (deleted IN (0, 1)),
lastWriteTime INTEGER NOT NULL,
data json
);
`;
await sqliteBasics.run(
database,
{
query: tableQuery,
params: [],
context: {
method: 'createSQLiteStorageInstance create tables',
data: params.databaseName
}
}
);
return 'COMMIT';
},
{
indexCreation: false,
databaseName: params.databaseName,
collectionName: params.collectionName
}
);
return database;
});
const instance = new RxStorageInstanceSQLite(
storage,
params.databaseName,
params.collectionName,
params.schema,
internals as any,
params.options,
settings,
tableName,
params.devMode
);
await addRxStorageMultiInstanceSupport(
RX_STORAGE_NAME_SQLITE,
params,
instance
);
return instance;
}