From a626930e7250884f120615990614cddfadbbb59e Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 8 Feb 2022 14:38:35 +0100 Subject: [PATCH 1/8] MOBILE-3981 core: Optimize filepool tables --- .../classes/database/database-table-proxy.ts | 40 +++- src/core/classes/database/database-table.ts | 172 +++++++++++++++- .../classes/database/debug-database-table.ts | 50 ++++- .../classes/database/eager-database-table.ts | 61 +++++- .../classes/database/lazy-database-table.ts | 31 ++- src/core/classes/sqlitedb.ts | 88 ++++----- src/core/classes/tests/database-table.test.ts | 40 +++- src/core/features/h5p/classes/framework.ts | 3 +- src/core/services/filepool.ts | 186 +++++++++--------- 9 files changed, 489 insertions(+), 182 deletions(-) diff --git a/src/core/classes/database/database-table-proxy.ts b/src/core/classes/database/database-table-proxy.ts index bda6135bc..d9d676bcc 100644 --- a/src/core/classes/database/database-table-proxy.ts +++ b/src/core/classes/database/database-table-proxy.ts @@ -17,7 +17,13 @@ import { asyncInstance } from '@/core/utils/async-instance'; import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; import { CoreConfig, CoreConfigProvider } from '@services/config'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreDatabaseReducer, CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table'; +import { + CoreDatabaseReducer, + CoreDatabaseTable, + CoreDatabaseConditions, + GetDBRecordPrimaryKey, + CoreDatabaseQueryOptions, +} from './database-table'; import { CoreDebugDatabaseTable } from './debug-database-table'; import { CoreEagerDatabaseTable } from './eager-database-table'; import { CoreLazyDatabaseTable } from './lazy-database-table'; @@ -67,15 +73,25 @@ export class CoreDatabaseTableProxy< /** * @inheritdoc */ - async getMany(conditions?: Partial): Promise { - return this.target.getMany(conditions); + async getMany(conditions?: Partial, options?: Partial>): Promise { + return this.target.getMany(conditions, options); } /** * @inheritdoc */ - async getOne(conditions: Partial): Promise { - return this.target.getOne(conditions); + getManyWhere(conditions: CoreDatabaseConditions): Promise { + return this.target.getManyWhere(conditions); + } + + /** + * @inheritdoc + */ + async getOne( + conditions?: Partial, + options?: Partial, 'offset' | 'limit'>>, + ): Promise { + return this.target.getOne(conditions, options); } /** @@ -92,6 +108,20 @@ export class CoreDatabaseTableProxy< return this.target.reduce(reducer, conditions); } + /** + * @inheritdoc + */ + hasAny(conditions?: Partial): Promise { + return this.target.hasAny(conditions); + } + + /** + * @inheritdoc + */ + count(conditions?: Partial): Promise { + return this.target.count(conditions); + } + /** * @inheritdoc */ diff --git a/src/core/classes/database/database-table.ts b/src/core/classes/database/database-table.ts index f018b062c..6ff8f92d5 100644 --- a/src/core/classes/database/database-table.ts +++ b/src/core/classes/database/database-table.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreError } from '@classes/errors/error'; import { SQLiteDB, SQLiteDBRecordValue, SQLiteDBRecordValues } from '@classes/sqlitedb'; /** @@ -78,22 +79,57 @@ export class CoreDatabaseTable< * Get records matching the given conditions. * * @param conditions Matching conditions. If this argument is missing, all records in the table will be returned. + * @param options Query options. * @returns Database records. */ - getMany(conditions?: Partial): Promise { - return conditions - ? this.database.getRecords(this.tableName, conditions) - : this.database.getAllRecords(this.tableName); + getMany(conditions?: Partial, options?: Partial>): Promise { + if (!conditions && !options) { + return this.database.getAllRecords(this.tableName); + } + + const sorting = options?.sorting + && this.normalizedSorting(options.sorting).map(([column, direction]) => `${column} ${direction}`).join(', '); + + return this.database.getRecords(this.tableName, conditions, sorting, '*', options?.offset, options?.limit); + } + + /** + * Get records matching the given conditions. + * + * This method should be used when it's necessary to apply complex conditions; the simple `getMany` + * method should be favored otherwise for better performance. + * + * @param conditions Matching conditions in SQL and JavaScript. + */ + getManyWhere(conditions: CoreDatabaseConditions): Promise { + return this.database.getRecordsSelect(this.tableName, conditions.sql, conditions.sqlParams); } /** * Find one record matching the given conditions. * * @param conditions Matching conditions. + * @param options Result options. * @returns Database record. */ - getOne(conditions: Partial): Promise { - return this.database.getRecord(this.tableName, conditions); + async getOne( + conditions?: Partial, + options?: Partial, 'offset' | 'limit'>>, + ): Promise { + if (!options) { + return this.database.getRecord(this.tableName, conditions); + } + + const records = await this.getMany(conditions, { + ...options, + limit: 1, + }); + + if (records.length === 0) { + throw new CoreError('No records found.'); + } + + return records[0]; } /** @@ -121,6 +157,43 @@ export class CoreDatabaseTable< ) as unknown as Promise; } + /** + * Check whether the table is empty or not. + * + * @returns Whether the table is empty or not. + */ + isEmpty(): Promise { + return this.hasAny(); + } + + /** + * Check whether the table has any record matching the given conditions. + * + * @param conditions Matching conditions. If this argument is missing, this method will return whether the table + * is empty or not. + * @returns Whether the table contains any records matching the given conditions. + */ + async hasAny(conditions?: Partial): Promise { + try { + await this.getOne(conditions); + + return true; + } catch (error) { + // Couldn't get a single record. + return false; + } + } + + /** + * Count records in table. + * + * @param conditions Matching conditions. + * @returns Number of records matching the given conditions. + */ + count(conditions?: Partial): Promise { + return this.database.countRecords(this.tableName, conditions); + } + /** * Insert a new record. * @@ -208,6 +281,59 @@ export class CoreDatabaseTable< return !Object.entries(conditions).some(([column, value]) => record[column] !== value); } + /** + * Sort a list of records with the given order. This method mutates the input array. + * + * @param records Array of records to sort. + * @param sorting Sorting conditions. + * @returns Sorted array. This will be the same reference that was given as an argument. + */ + protected sortRecords(records: DBRecord[], sorting: CoreDatabaseSorting): DBRecord[] { + const columnsSorting = this.normalizedSorting(sorting); + + records.sort((a, b) => { + for (const [column, direction] of columnsSorting) { + const aValue = a[column]; + const bValue = b[column]; + + if (aValue > bValue) { + return direction === 'desc' ? -1 : 1; + } + + if (aValue < bValue) { + return direction === 'desc' ? 1 : -1; + } + } + + return 0; + }); + + return records; + } + + /** + * Get a normalized array of sorting conditions. + * + * @param sorting Sorting conditions. + * @returns Normalized sorting conditions. + */ + protected normalizedSorting(sorting: CoreDatabaseSorting): [keyof DBRecord, 'asc' | 'desc'][] { + const sortingArray = Array.isArray(sorting) ? sorting : [sorting]; + + return sortingArray.reduce((normalizedSorting, columnSorting) => { + normalizedSorting.push( + typeof columnSorting === 'object' + ? [ + Object.keys(columnSorting)[0] as keyof DBRecord, + Object.values(columnSorting)[0] as 'asc' | 'desc', + ] + : [columnSorting, 'asc'], + ); + + return normalizedSorting; + }, [] as [keyof DBRecord, 'asc' | 'desc'][]); + } + } /** @@ -238,3 +364,37 @@ export type CoreDatabaseConditions = { sqlParams?: SQLiteDBRecordValue[]; js: (record: DBRecord) => boolean; }; + +/** + * Sorting conditions for a single column. + * + * This type will accept an object that defines sorting conditions for a single column, but not more. + * For example, `{id: 'desc'}` and `{name: 'asc'}` would be acceptend values, but `{id: 'desc', name: 'asc'}` wouldn't. + * + * @see https://stackoverflow.com/questions/57571664/typescript-type-for-an-object-with-only-one-key-no-union-type-allowed-as-a-key + */ +export type CoreDatabaseColumnSorting = { + [Column in DBRecordColumn]: + (Record & Partial, never>>) extends infer ColumnSorting + ? { [Column in keyof ColumnSorting]: ColumnSorting[Column] } + : never; +}[DBRecordColumn]; + +/** + * Sorting conditions to apply to query results. + * + * Columns will be sorted in ascending order by default. + */ +export type CoreDatabaseSorting = + keyof DBRecord | + CoreDatabaseColumnSorting | + Array>; + +/** + * Options to configure query results. + */ +export type CoreDatabaseQueryOptions = { + offset: number; + limit: number; + sorting: CoreDatabaseSorting; +}; diff --git a/src/core/classes/database/debug-database-table.ts b/src/core/classes/database/debug-database-table.ts index 209c9abb1..f944b8a01 100644 --- a/src/core/classes/database/debug-database-table.ts +++ b/src/core/classes/database/debug-database-table.ts @@ -14,7 +14,13 @@ import { SQLiteDBRecordValues } from '@classes/sqlitedb'; import { CoreLogger } from '@singletons/logger'; -import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseReducer } from './database-table'; +import { + CoreDatabaseTable, + CoreDatabaseConditions, + GetDBRecordPrimaryKey, + CoreDatabaseReducer, + CoreDatabaseQueryOptions, +} from './database-table'; /** * Database table proxy used to debug runtime operations. @@ -58,19 +64,31 @@ export class CoreDebugDatabaseTable< /** * @inheritdoc */ - getMany(conditions?: Partial): Promise { - this.logger.log('getMany', conditions); + getMany(conditions?: Partial, options?: Partial>): Promise { + this.logger.log('getMany', conditions, options); - return this.target.getMany(conditions); + return this.target.getMany(conditions, options); } /** * @inheritdoc */ - getOne(conditions: Partial): Promise { - this.logger.log('getOne', conditions); + getManyWhere(conditions: CoreDatabaseConditions): Promise { + this.logger.log('getManyWhere', conditions); - return this.target.getOne(conditions); + return this.target.getManyWhere(conditions); + } + + /** + * @inheritdoc + */ + getOne( + conditions?: Partial, + options?: Partial, 'offset' | 'limit'>>, + ): Promise { + this.logger.log('getOne', conditions, options); + + return this.target.getOne(conditions, options); } /** @@ -91,6 +109,24 @@ export class CoreDebugDatabaseTable< return this.target.reduce(reducer, conditions); } + /** + * @inheritdoc + */ + hasAny(conditions?: Partial): Promise { + this.logger.log('hasAny', conditions); + + return this.target.hasAny(conditions); + } + + /** + * @inheritdoc + */ + count(conditions?: Partial): Promise { + this.logger.log('count', conditions); + + return this.target.count(conditions); + } + /** * @inheritdoc */ diff --git a/src/core/classes/database/eager-database-table.ts b/src/core/classes/database/eager-database-table.ts index 37b09342c..461485df5 100644 --- a/src/core/classes/database/eager-database-table.ts +++ b/src/core/classes/database/eager-database-table.ts @@ -14,7 +14,13 @@ import { CoreError } from '@classes/errors/error'; import { SQLiteDBRecordValues } from '@classes/sqlitedb'; -import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseReducer } from './database-table'; +import { + CoreDatabaseTable, + CoreDatabaseConditions, + GetDBRecordPrimaryKey, + CoreDatabaseReducer, + CoreDatabaseQueryOptions, +} from './database-table'; /** * Wrapper used to improve performance by caching all the records for faster read operations. @@ -48,21 +54,44 @@ export class CoreEagerDatabaseTable< /** * @inheritdoc */ - async getMany(conditions?: Partial): Promise { + async getMany(conditions?: Partial, options?: Partial>): Promise { const records = Object.values(this.records); - - return conditions + const filteredRecords = conditions ? records.filter(record => this.recordMatches(record, conditions)) : records; + + if (options?.sorting) { + this.sortRecords(filteredRecords, options.sorting); + } + + return filteredRecords.slice(options?.offset ?? 0, options?.limit); } /** * @inheritdoc */ - async getOne(conditions: Partial): Promise { - const record = Object.values(this.records).find(record => this.recordMatches(record, conditions)) ?? null; + async getManyWhere(conditions: CoreDatabaseConditions): Promise { + return Object.values(this.records).filter(record => conditions.js(record)); + } - if (record === null) { + /** + * @inheritdoc + */ + async getOne( + conditions?: Partial, + options?: Partial, 'offset' | 'limit'>>, + ): Promise { + let record: DBRecord | undefined; + + if (options?.sorting) { + record = this.getMany(conditions, { ...options, limit: 1 })[0]; + } else if (conditions) { + record = Object.values(this.records).find(record => this.recordMatches(record, conditions)); + } else { + record = Object.values(this.records)[0]; + } + + if (!record) { throw new CoreError('No records found.'); } @@ -94,6 +123,24 @@ export class CoreEagerDatabaseTable< ); } + /** + * @inheritdoc + */ + async hasAny(conditions?: Partial): Promise { + return conditions + ? Object.values(this.records).some(record => this.recordMatches(record, conditions)) + : Object.values(this.records).length > 0; + } + + /** + * @inheritdoc + */ + async count(conditions?: Partial): Promise { + return conditions + ? Object.values(this.records).filter(record => this.recordMatches(record, conditions)).length + : Object.values(this.records).length; + } + /** * @inheritdoc */ diff --git a/src/core/classes/database/lazy-database-table.ts b/src/core/classes/database/lazy-database-table.ts index 9ec890a20..489b21a54 100644 --- a/src/core/classes/database/lazy-database-table.ts +++ b/src/core/classes/database/lazy-database-table.ts @@ -14,7 +14,7 @@ import { CoreError } from '@classes/errors/error'; import { SQLiteDBRecordValues } from '@classes/sqlitedb'; -import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey } from './database-table'; +import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseQueryOptions } from './database-table'; /** * Wrapper used to improve performance by caching records that are used often for faster read operations. @@ -33,15 +33,13 @@ export class CoreLazyDatabaseTable< /** * @inheritdoc */ - async getOne(conditions: Partial): Promise { - let record: DBRecord | null = - Object.values(this.records).find(record => record && this.recordMatches(record, conditions)) ?? null; + async getOne( + conditions?: Partial, + options?: Partial, 'offset' | 'limit'>>, + ): Promise { + const record = await super.getOne(conditions, options); - if (!record) { - record = await super.getOne(conditions); - - this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record; - } + this.records[this.serializePrimaryKey(this.getPrimaryKeyFromRecord(record))] = record; return record; } @@ -75,6 +73,21 @@ export class CoreLazyDatabaseTable< return record; } + /** + * @inheritdoc + */ + async hasAny(conditions?: Partial): Promise { + const hasAnyMatching = Object + .values(this.records) + .some(record => record !== null && (!conditions || this.recordMatches(record, conditions))); + + if (hasAnyMatching) { + return true; + } + + return super.hasAny(conditions); + } + /** * @inheritdoc */ diff --git a/src/core/classes/sqlitedb.ts b/src/core/classes/sqlitedb.ts index 915ea9ffb..fc5f762e9 100644 --- a/src/core/classes/sqlitedb.ts +++ b/src/core/classes/sqlitedb.ts @@ -136,6 +136,50 @@ export interface SQLiteDBForeignKeySchema { */ export class SQLiteDB { + /** + * Constructs 'IN()' or '=' sql fragment + * + * @param items A single value or array of values for the expression. It doesn't accept objects. + * @param equal True means we want to equate to the constructed expression. + * @param onEmptyItems This defines the behavior when the array of items provided is empty. Defaults to false, + * meaning return empty. Other values will become part of the returned SQL fragment. + * @return A list containing the constructed sql fragment and an array of parameters. + */ + static getInOrEqual( + items: SQLiteDBRecordValue | SQLiteDBRecordValue[], + equal: boolean = true, + onEmptyItems?: SQLiteDBRecordValue | null, + ): SQLiteDBQueryParams { + let sql = ''; + let params: SQLiteDBRecordValue[]; + + // Default behavior, return empty data on empty array. + if (Array.isArray(items) && !items.length && onEmptyItems === undefined) { + return { sql: '', params: [] }; + } + + // Handle onEmptyItems on empty array of items. + if (Array.isArray(items) && !items.length) { + if (onEmptyItems === null) { // Special case, NULL value. + sql = equal ? ' IS NULL' : ' IS NOT NULL'; + + return { sql, params: [] }; + } else { + items = [onEmptyItems as SQLiteDBRecordValue]; // Rest of cases, prepare items for processing. + } + } + + if (!Array.isArray(items) || items.length == 1) { + sql = equal ? '= ?' : '<> ?'; + params = Array.isArray(items) ? items : [items]; + } else { + sql = (equal ? '' : 'NOT ') + 'IN (' + ',?'.repeat(items.length).substring(1) + ')'; + params = items; + } + + return { sql, params }; + } + db?: SQLiteObject; promise!: Promise; @@ -564,50 +608,6 @@ export class SQLiteDB { return record[Object.keys(record)[0]]; } - /** - * Constructs 'IN()' or '=' sql fragment - * - * @param items A single value or array of values for the expression. It doesn't accept objects. - * @param equal True means we want to equate to the constructed expression. - * @param onEmptyItems This defines the behavior when the array of items provided is empty. Defaults to false, - * meaning return empty. Other values will become part of the returned SQL fragment. - * @return A list containing the constructed sql fragment and an array of parameters. - */ - getInOrEqual( - items: SQLiteDBRecordValue | SQLiteDBRecordValue[], - equal: boolean = true, - onEmptyItems?: SQLiteDBRecordValue | null, - ): SQLiteDBQueryParams { - let sql = ''; - let params: SQLiteDBRecordValue[]; - - // Default behavior, return empty data on empty array. - if (Array.isArray(items) && !items.length && onEmptyItems === undefined) { - return { sql: '', params: [] }; - } - - // Handle onEmptyItems on empty array of items. - if (Array.isArray(items) && !items.length) { - if (onEmptyItems === null || onEmptyItems === undefined) { // Special case, NULL value. - sql = equal ? ' IS NULL' : ' IS NOT NULL'; - - return { sql, params: [] }; - } else { - items = [onEmptyItems]; // Rest of cases, prepare items for processing. - } - } - - if (!Array.isArray(items) || items.length == 1) { - sql = equal ? '= ?' : '<> ?'; - params = Array.isArray(items) ? items : [items]; - } else { - sql = (equal ? '' : 'NOT ') + 'IN (' + ',?'.repeat(items.length).substring(1) + ')'; - params = items; - } - - return { sql, params }; - } - /** * Get the database name. * diff --git a/src/core/classes/tests/database-table.test.ts b/src/core/classes/tests/database-table.test.ts index a942959c1..2cf4840b0 100644 --- a/src/core/classes/tests/database-table.test.ts +++ b/src/core/classes/tests/database-table.test.ts @@ -13,20 +13,20 @@ // limitations under the License. import { mock, mockSingleton } from '@/testing/utils'; -import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseSorting, CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseCachingStrategy, CoreDatabaseConfiguration, CoreDatabaseTableProxy, } from '@classes/database/database-table-proxy'; -import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; +import { SQLiteDB } from '@classes/sqlitedb'; import { CoreConfig } from '@services/config'; -interface User extends SQLiteDBRecordValues { +type User = { id: number; name: string; surname: string; -} +}; function userMatches(user: User, conditions: Partial) { return !Object.entries(conditions).some(([column, value]) => user[column] !== value); @@ -45,7 +45,7 @@ function prepareStubs(config: Partial = {}): [User[], return record as unknown as T; }, getRecords: async (_, conditions) => records.filter(record => userMatches(record, conditions)) as unknown as T[], - getAllRecords: async () => records as unknown as T[], + getAllRecords: async () => records.slice(0) as unknown as T[], deleteRecords: async (_, conditions) => { const usersToDelete: User[] = []; @@ -81,10 +81,10 @@ async function testFindItems(records: User[], table: CoreDatabaseTable) { await table.initialize(); - await expect(table.getOneByPrimaryKey({ id: 1 })).resolves.toEqual(john); - await expect(table.getOneByPrimaryKey({ id: 2 })).resolves.toEqual(amy); await expect(table.getOne({ surname: 'Doe', name: 'John' })).resolves.toEqual(john); await expect(table.getOne({ surname: 'Doe', name: 'Amy' })).resolves.toEqual(amy); + await expect(table.getOneByPrimaryKey({ id: 1 })).resolves.toEqual(john); + await expect(table.getOneByPrimaryKey({ id: 2 })).resolves.toEqual(amy); } async function testInsertItems(records: User[], database: SQLiteDB, table: CoreDatabaseTable) { @@ -165,6 +165,32 @@ describe('CoreDatabaseTable with eager caching', () => { expect(database.getRecord).not.toHaveBeenCalled(); }); + it('sorts items', async () => { + // Arrange. + const john = { id: 1, name: 'John', surname: 'Doe' }; + const amy = { id: 2, name: 'Amy', surname: 'Doe' }; + const jane = { id: 3, name: 'Jane', surname: 'Smith' }; + const expectSorting = async (sorting: CoreDatabaseSorting, expectedResults: User[]) => { + const results = await table.getMany({}, { sorting }); + + expect(results).toEqual(expectedResults); + }; + + records.push(john); + records.push(amy); + records.push(jane); + + await table.initialize(); + + // Act & Assert. + await expectSorting('name', [amy, jane, john]); + await expectSorting('surname', [john, amy, jane]); + await expectSorting({ name: 'desc' }, [john, jane, amy]); + await expectSorting({ surname: 'desc' }, [jane, john, amy]); + await expectSorting(['name', { surname: 'desc' }], [amy, jane, john]); + await expectSorting([{ surname: 'desc' }, 'name'], [jane, amy, john]); + }); + it('inserts items', () => testInsertItems(records, database, table)); it('deletes items', () => testDeleteItems(records, database, table)); it('deletes items by primary key', () => testDeleteItemsByPrimaryKey(records, database, table)); diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index 6386d853e..8b20463dc 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -43,6 +43,7 @@ import { CoreH5PContentBeingSaved, CoreH5PLibraryBeingSaved } from './storage'; import { CoreH5PLibraryAddTo, CoreH5PLibraryMetadataSettings } from './validator'; import { CoreH5PMetadata } from './metadata'; import { Translate } from '@singletons'; +import { SQLiteDB } from '@classes/sqlitedb'; /** * Equivalent to Moodle's implementation of H5PFrameworkInterface. @@ -64,7 +65,7 @@ export class CoreH5PFramework { const db = await CoreSites.getSiteDb(siteId); - const whereAndParams = db.getInOrEqual(libraryIds); + const whereAndParams = SQLiteDB.getInOrEqual(libraryIds); whereAndParams.sql = 'mainlibraryid ' + whereAndParams.sql; await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams.sql, whereAndParams.params); diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 06b54fb61..fd11e1fe5 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -49,7 +49,7 @@ import { import { CoreFileHelper } from './file-helper'; import { CoreUrl } from '@singletons/url'; import { CoreDatabaseTable } from '@classes/database/database-table'; -import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; import { lazyMap, LazyMap } from '../utils/lazy-map'; import { asyncInstance, AsyncInstance } from '../utils/async-instance'; @@ -98,14 +98,14 @@ export class CoreFilepoolProvider { // Variables to prevent downloading packages/files twice at the same time. protected packagesPromises: { [s: string]: { [s: string]: Promise } } = {}; protected filePromises: { [s: string]: { [s: string]: Promise } } = {}; - - // Variables for DB. - protected appDB: Promise; - protected resolveAppDB!: (appDB: SQLiteDB) => void; protected filesTables: LazyMap>>; + protected linksTables: + LazyMap>>; + + protected packagesTables: LazyMap>>; + protected queueTable = asyncInstance>(); constructor() { - this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreFilepoolProvider'); this.filesTables = lazyMap( siteId => asyncInstance( @@ -116,6 +116,23 @@ export class CoreFilepoolProvider { }), ), ); + this.linksTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(LINKS_TABLE_NAME, { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, + primaryKeyColumns: ['fileId', 'component', 'componentId'], + }), + ), + ); + this.packagesTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(PACKAGES_TABLE_NAME, { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, + }), + ), + ); } /** @@ -154,7 +171,16 @@ export class CoreFilepoolProvider { // Ignore errors. } - this.resolveAppDB(CoreApp.getDB()); + const queueTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, + CoreApp.getDB(), + QUEUE_TABLE_NAME, + ['siteId','fileId'], + ); + + await queueTable.initialize(); + + this.queueTable.setInstance(queueTable); } /** @@ -171,16 +197,11 @@ export class CoreFilepoolProvider { throw new CoreError('Cannot add link because component is invalid.'); } - componentId = this.fixComponentId(componentId); - - const db = await CoreSites.getSiteDb(siteId); - const newEntry: CoreFilepoolLinksRecord = { + await this.linksTables[siteId].insert({ fileId, component, - componentId: componentId || '', - }; - - await db.insertRecord(LINKS_TABLE_NAME, newEntry); + componentId: this.fixComponentId(componentId) || '', + }); } /** @@ -301,9 +322,7 @@ export class CoreFilepoolProvider { ): Promise { this.logger.debug(`Adding ${fileId} to the queue`); - const db = await this.appDB; - - await db.insertRecord(QUEUE_TABLE_NAME, { + await this.queueTable.insert({ siteId, fileId, url, @@ -431,10 +450,7 @@ export class CoreFilepoolProvider { // Update only when required. this.logger.debug(`Updating file ${fileId} which is already in queue`); - const db = await this.appDB; - - return db.updateRecords(QUEUE_TABLE_NAME, newData, primaryKey).then(() => - this.getQueuePromise(siteId, fileId, true, onProgress)); + return this.queueTable.update(newData, primaryKey).then(() => this.getQueuePromise(siteId, fileId, true, onProgress)); } this.logger.debug(`File ${fileId} already in queue and does not require update`); @@ -560,11 +576,10 @@ export class CoreFilepoolProvider { async clearAllPackagesStatus(siteId: string): Promise { this.logger.debug('Clear all packages status for site ' + siteId); - const site = await CoreSites.getSite(siteId); // Get all the packages to be able to "notify" the change in the status. - const entries: CoreFilepoolPackageEntry[] = await site.getDb().getAllRecords(PACKAGES_TABLE_NAME); + const entries = await this.packagesTables[siteId].getMany(); // Delete all the entries. - await site.getDb().deleteRecords(PACKAGES_TABLE_NAME); + await this.packagesTables[siteId].delete(); entries.forEach((entry) => { if (!entry.component) { @@ -583,15 +598,13 @@ export class CoreFilepoolProvider { * @return Promise resolved when the filepool is cleared. */ async clearFilepool(siteId: string): Promise { - const db = await CoreSites.getSiteDb(siteId); - // Read the data first to be able to notify the deletions. const filesEntries = await this.filesTables[siteId].getMany(); - const filesLinks = await db.getAllRecords(LINKS_TABLE_NAME); + const filesLinks = await this.linksTables[siteId].getMany(); await Promise.all([ this.filesTables[siteId].delete(), - db.deleteRecords(LINKS_TABLE_NAME), + this.linksTables[siteId].delete(), ]); // Notify now. @@ -609,14 +622,14 @@ export class CoreFilepoolProvider { * @return Resolved means yes, rejected means no. */ async componentHasFiles(siteId: string, component: string, componentId?: string | number): Promise { - const db = await CoreSites.getSiteDb(siteId); const conditions = { component, componentId: this.fixComponentId(componentId), }; - const count = await db.countRecords(LINKS_TABLE_NAME, conditions); - if (count <= 0) { + const hasAnyLinks = await this.linksTables[siteId].hasAny(conditions); + + if (!hasAnyLinks) { throw new CoreError('Component doesn\'t have files'); } } @@ -1144,7 +1157,6 @@ export class CoreFilepoolProvider { return; } - const db = await CoreSites.getSiteDb(siteId); const extension = CoreMimetypeUtils.getFileExtension(entry.path); if (!extension) { // Files does not have extension. Invalidate file (stale = true). @@ -1170,7 +1182,7 @@ export class CoreFilepoolProvider { } // Now update the links. - await db.updateRecords(LINKS_TABLE_NAME, { fileId: entry.fileId }, { fileId }); + await this.linksTables[siteId].update({ fileId: entry.fileId }, { fileId }); } /** @@ -1228,16 +1240,18 @@ export class CoreFilepoolProvider { * @return Promise resolved with the files. */ protected async getComponentFiles( - db: SQLiteDB, + siteId: string | undefined, component: string, componentId?: string | number, ): Promise { + siteId = siteId ?? CoreSites.getCurrentSiteId(); const conditions = { component, componentId: this.fixComponentId(componentId), }; - const items = await db.getRecords(LINKS_TABLE_NAME, conditions); + const items = await this.linksTables[siteId].getMany(conditions); + items.forEach((item) => { item.componentId = this.fixComponentId(item.componentId); }); @@ -1349,8 +1363,7 @@ export class CoreFilepoolProvider { * @return Promise resolved with the links. */ protected async getFileLinks(siteId: string, fileId: string): Promise { - const db = await CoreSites.getSiteDb(siteId); - const items = await db.getRecords(LINKS_TABLE_NAME, { fileId }); + const items = await this.linksTables[siteId].getMany({ fileId }); items.forEach((item) => { item.componentId = this.fixComponentId(item.componentId); @@ -1421,8 +1434,7 @@ export class CoreFilepoolProvider { * @return Promise resolved with the files on success. */ async getFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { - const db = await CoreSites.getSiteDb(siteId); - const items = await this.getComponentFiles(db, component, componentId); + const items = await this.getComponentFiles(siteId, component, componentId); const files: CoreFilepoolFileEntry[] = []; await Promise.all(items.map(async (item) => { @@ -1706,10 +1718,9 @@ export class CoreFilepoolProvider { async getPackageData(siteId: string, component: string, componentId?: string | number): Promise { componentId = this.fixComponentId(componentId); - const site = await CoreSites.getSite(siteId); const packageId = this.getPackageId(component, componentId); - return site.getDb().getRecord(PACKAGES_TABLE_NAME, { id: packageId }); + return this.packagesTables[siteId].getOneByPrimaryKey({ id: packageId }); } /** @@ -2171,16 +2182,16 @@ export class CoreFilepoolProvider { * @return Resolved with file object from DB on success, rejected otherwise. */ protected async hasFileInQueue(siteId: string, fileId: string): Promise { - const db = await this.appDB; - const entry = await db.getRecord(QUEUE_TABLE_NAME, { siteId, fileId }); + const entry = await this.queueTable.getOneByPrimaryKey({ siteId, fileId }); if (entry === undefined) { throw new CoreError('File not found in queue.'); } - // Convert the links to an object. - entry.linksUnserialized = CoreTextUtils.parseJSON(entry.links || '[]', []); - return entry; + return { + ...entry, + linksUnserialized: CoreTextUtils.parseJSON(entry.links, []), + }; } /** @@ -2238,8 +2249,7 @@ export class CoreFilepoolProvider { componentId?: string | number, onlyUnknown: boolean = true, ): Promise { - const db = await CoreSites.getSiteDb(siteId); - const items = await this.getComponentFiles(db, component, componentId); + const items = await this.getComponentFiles(siteId, component, componentId); if (!items.length) { // Nothing to invalidate. @@ -2250,7 +2260,7 @@ export class CoreFilepoolProvider { const fileIds = items.map((item) => item.fileId); - const whereAndParams = db.getInOrEqual(fileIds); + const whereAndParams = SQLiteDB.getInOrEqual(fileIds); whereAndParams.sql = 'fileId ' + whereAndParams.sql; @@ -2523,30 +2533,25 @@ export class CoreFilepoolProvider { * @return Resolved on success. Rejected on failure. */ protected async processImportantQueueItem(): Promise { - let items: CoreFilepoolQueueEntry[]; - const db = await this.appDB; - try { - items = await db.getRecords( - QUEUE_TABLE_NAME, - undefined, - 'priority DESC, added ASC', - undefined, - 0, - 1, - ); + const item = await this.queueTable.getOne({}, { + sorting: [ + { priority: 'desc' }, + { added: 'asc' }, + ], + }); + + if (!item) { + throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; + } + + return this.processQueueItem({ + ...item, + linksUnserialized: CoreTextUtils.parseJSON(item.links, []), + }); } catch (err) { throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; } - - const item = items.pop(); - if (!item) { - throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; - } - // Convert the links to an object. - item.linksUnserialized = CoreTextUtils.parseJSON(item.links, []); - - return this.processQueueItem(item); } /** @@ -2671,9 +2676,7 @@ export class CoreFilepoolProvider { * @return Resolved on success. Rejected on failure. It is advised to silently ignore failures. */ protected async removeFromQueue(siteId: string, fileId: string): Promise { - const db = await this.appDB; - - await db.deleteRecords(QUEUE_TABLE_NAME, { siteId, fileId }); + await this.queueTable.deleteByPrimaryKey({ siteId, fileId }); } /** @@ -2684,8 +2687,6 @@ export class CoreFilepoolProvider { * @return Resolved on success. */ protected async removeFileById(siteId: string, fileId: string): Promise { - const db = await CoreSites.getSiteDb(siteId); - // Get the path to the file first since it relies on the file object stored in the pool. // Don't use getFilePath to prevent performing 2 DB requests. let path = this.getFilepoolFolderPath(siteId) + '/' + fileId; @@ -2714,7 +2715,7 @@ export class CoreFilepoolProvider { promises.push(this.filesTables[siteId].delete(conditions)); // Remove links. - promises.push(db.deleteRecords(LINKS_TABLE_NAME, conditions)); + promises.push(this.linksTables[siteId].delete(conditions)); // Remove the file. if (CoreFile.isAvailable()) { @@ -2745,8 +2746,7 @@ export class CoreFilepoolProvider { * @return Resolved on success. */ async removeFilesByComponent(siteId: string, component: string, componentId?: string | number): Promise { - const db = await CoreSites.getSiteDb(siteId); - const items = await this.getComponentFiles(db, component, componentId); + const items = await this.getComponentFiles(siteId, component, componentId); await Promise.all(items.map((item) => this.removeFileById(siteId, item.fileId))); } @@ -2795,11 +2795,10 @@ export class CoreFilepoolProvider { componentId = this.fixComponentId(componentId); this.logger.debug(`Set previous status for package ${component} ${componentId}`); - const site = await CoreSites.getSite(siteId); const packageId = this.getPackageId(component, componentId); // Get current stored data, we'll only update 'status' and 'updated' fields. - const entry = site.getDb().getRecord(PACKAGES_TABLE_NAME, { id: packageId }); + const entry = await this.packagesTables[siteId].getOneByPrimaryKey({ id: packageId }); const newData: CoreFilepoolPackageEntry = {}; if (entry.status == CoreConstants.DOWNLOADING) { // Going back from downloading to previous status, restore previous download time. @@ -2809,9 +2808,9 @@ export class CoreFilepoolProvider { newData.updated = Date.now(); this.logger.debug(`Set previous status '${entry.status}' for package ${component} ${componentId}`); - await site.getDb().updateRecords(PACKAGES_TABLE_NAME, newData, { id: packageId }); + await this.packagesTables[siteId].update(newData, { id: packageId }); // Success updating, trigger event. - this.triggerPackageStatusChanged(site.getId(), newData.status, component, componentId); + this.triggerPackageStatusChanged(siteId, newData.status, component, componentId); return newData.status; } @@ -2900,7 +2899,6 @@ export class CoreFilepoolProvider { this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`); componentId = this.fixComponentId(componentId); - const site = await CoreSites.getSite(siteId); const packageId = this.getPackageId(component, componentId); let downloadTime: number | undefined; let previousDownloadTime: number | undefined; @@ -2913,7 +2911,7 @@ export class CoreFilepoolProvider { let previousStatus: string | undefined; // Search current status to set it as previous status. try { - const entry = await site.getDb().getRecord(PACKAGES_TABLE_NAME, { id: packageId }); + const entry = await this.packagesTables[siteId].getOneByPrimaryKey({ id: packageId }); extra = extra ?? entry.extra; if (downloadTime === undefined) { @@ -2930,7 +2928,12 @@ export class CoreFilepoolProvider { // No previous status. } - const packageEntry: CoreFilepoolPackageEntry = { + if (previousStatus === status) { + // The package already has this status, no need to change it. + return; + } + + await this.packagesTables[siteId].insert({ id: packageId, component, componentId, @@ -2940,14 +2943,7 @@ export class CoreFilepoolProvider { downloadTime, previousDownloadTime, extra, - }; - - if (previousStatus === status) { - // The package already has this status, no need to change it. - return; - } - - await site.getDb().insertRecord(PACKAGES_TABLE_NAME, packageEntry); + }); // Success inserting, trigger event. this.triggerPackageStatusChanged(siteId, status, component, componentId); @@ -3067,11 +3063,9 @@ export class CoreFilepoolProvider { async updatePackageDownloadTime(siteId: string, component: string, componentId?: string | number): Promise { componentId = this.fixComponentId(componentId); - const site = await CoreSites.getSite(siteId); const packageId = this.getPackageId(component, componentId); - await site.getDb().updateRecords( - PACKAGES_TABLE_NAME, + await this.packagesTables[siteId].update( { downloadTime: CoreTimeUtils.timestamp() }, { id: packageId }, ); From df2388297d9f54e6499ab6119509aad55d99f502 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 8 Feb 2022 14:46:51 +0100 Subject: [PATCH 2/8] MOBILE-3981 core: Optimize app tables --- src/core/services/app.ts | 68 +++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/src/core/services/app.ts b/src/core/services/app.ts index e0710a084..95031a923 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -25,14 +25,9 @@ import { CoreColors } from '@singletons/colors'; import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; import { CoreObject } from '@singletons/object'; import { CoreRedirectPayload } from './navigator'; - -/** - * Object responsible of managing schema versions. - */ -type SchemaVersionsManager = { - get(schemaName: string): Promise; - set(schemaName: string, version: number): Promise; -}; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; +import { asyncInstance } from '../utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; /** * Factory to provide some global functionalities, like access to the global app database. @@ -58,13 +53,9 @@ export class CoreAppProvider { protected keyboardClosing = false; protected forceOffline = false; protected redirect?: CoreRedirectData; - - // Variables for DB. - protected schemaVersionsManager: Promise; - protected resolveSchemaVersionsManager!: (schemaVersionsManager: SchemaVersionsManager) => void; + protected schemaVersionsTable = asyncInstance>(); constructor() { - this.schemaVersionsManager = new Promise(resolve => this.resolveSchemaVersionsManager = resolve); this.logger = CoreLogger.getInstance('CoreAppProvider'); } @@ -81,24 +72,20 @@ export class CoreAppProvider { * Initialize database. */ async initializeDatabase(): Promise { - await this.getDB().createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA); + const database = this.getDB(); - this.resolveSchemaVersionsManager({ - get: async name => { - try { - // Fetch installed version of the schema. - const entry = await this.getDB().getRecord(SCHEMA_VERSIONS_TABLE_NAME, { name }); + await database.createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA); - return entry.version; - } catch (error) { - // No installed version yet. - return 0; - } - }, - set: async (name, version) => { - await this.getDB().insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name, version }); - }, - }); + const schemaVersionsTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + database, + SCHEMA_VERSIONS_TABLE_NAME, + ['name'], + ); + + await schemaVersionsTable.initialize(); + + this.schemaVersionsTable.setInstance(schemaVersionsTable); } /** @@ -137,8 +124,7 @@ export class CoreAppProvider { async createTablesFromSchema(schema: CoreAppSchema): Promise { this.logger.debug(`Apply schema to app DB: ${schema.name}`); - const schemaVersionsManager = await this.schemaVersionsManager; - const oldVersion = await schemaVersionsManager.get(schema.name); + const oldVersion = await this.getInstalledSchemaVersion(schema); if (oldVersion >= schema.version) { // Version already installed, nothing else to do. @@ -155,7 +141,7 @@ export class CoreAppProvider { } // Set installed version. - schemaVersionsManager.set(schema.name, schema.version); + await this.schemaVersionsTable.insert({ name: schema.name, version: schema.version }); } /** @@ -683,6 +669,24 @@ export class CoreAppProvider { this.forceOffline = !!value; } + /** + * Get the installed version for the given schema. + * + * @param schema App schema. + * @returns Installed version number, or 0 if the schema is not installed. + */ + protected async getInstalledSchemaVersion(schema: CoreAppSchema): Promise { + try { + // Fetch installed version of the schema. + const entry = await this.schemaVersionsTable.getOneByPrimaryKey({ name: schema.name }); + + return entry.version; + } catch (error) { + // No installed version yet. + return 0; + } + } + } export const CoreApp = makeSingleton(CoreAppProvider); From cc006bad26e612953ceb8679098a163f8c7ef5eb Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Tue, 8 Feb 2022 14:47:39 +0100 Subject: [PATCH 3/8] MOBILE-3981 core: Optimize sites tables --- src/core/services/app.ts | 9 ++ src/core/services/config.ts | 16 +++ src/core/services/database/sites.ts | 22 ---- src/core/services/sites.ts | 152 ++++++++++++++++------------ 4 files changed, 112 insertions(+), 87 deletions(-) diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 95031a923..523418254 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -144,6 +144,15 @@ export class CoreAppProvider { await this.schemaVersionsTable.insert({ name: schema.name, version: schema.version }); } + /** + * Delete table schema. + * + * @param name Schema name. + */ + async deleteTableSchema(name: string): Promise { + await this.schemaVersionsTable.deleteByPrimaryKey({ name }); + } + /** * Get the application global database. * diff --git a/src/core/services/config.ts b/src/core/services/config.ts index b203690a3..f8d65ee23 100644 --- a/src/core/services/config.ts +++ b/src/core/services/config.ts @@ -120,6 +120,22 @@ export class CoreConfigProvider { } } + /** + * Check whether the given app setting exists. + * + * @param name The config name. + * @returns Whether the app setting exists. + */ + async has(name: string): Promise { + try { + await this.table.getOneByPrimaryKey({ name }); + + return true; + } catch (error) { + return false; + } + } + /** * Set an app setting. * diff --git a/src/core/services/database/sites.ts b/src/core/services/database/sites.ts index 857bdba4b..930955e46 100644 --- a/src/core/services/database/sites.ts +++ b/src/core/services/database/sites.ts @@ -21,7 +21,6 @@ import { CoreSite } from '@classes/site'; * Database variables for CoreSites service. */ export const SITES_TABLE_NAME = 'sites_2'; -export const CURRENT_SITE_TABLE_NAME = 'current_site'; export const SCHEMA_VERSIONS_TABLE_NAME = 'schema_versions'; // Schema to register in App DB. @@ -68,22 +67,6 @@ export const APP_SCHEMA: CoreAppSchema = { }, ], }, - { - name: CURRENT_SITE_TABLE_NAME, - columns: [ - { - name: 'id', - type: 'INTEGER', - primaryKey: true, - }, - { - name: 'siteId', - type: 'TEXT', - notNull: true, - unique: true, - }, - ], - }, ], async migrate(db: SQLiteDB, oldVersion: number): Promise { if (oldVersion < 2) { @@ -184,11 +167,6 @@ export type SiteDBEntry = { oauthId?: number | null; }; -export type CurrentSiteDBEntry = { - id: number; - siteId: string; -}; - export type SchemaVersionsDBEntry = { name: string; version: number; diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index ee324ae59..1dedfd9cf 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -41,10 +41,8 @@ import { APP_SCHEMA, SCHEMA_VERSIONS_TABLE_SCHEMA, SITES_TABLE_NAME, - CURRENT_SITE_TABLE_NAME, SCHEMA_VERSIONS_TABLE_NAME, SiteDBEntry, - CurrentSiteDBEntry, SchemaVersionsDBEntry, } from '@services/database/sites'; import { CoreArray } from '../singletons/array'; @@ -59,7 +57,13 @@ import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CorePromisedValue } from '@classes/promised-value'; import { CoreDatabaseTable } from '@classes/database/database-table'; -import { CoreDatabaseConfiguration, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; +import { + CoreDatabaseCachingStrategy, + CoreDatabaseConfiguration, + CoreDatabaseTableProxy, +} from '@classes/database/database-table-proxy'; +import { asyncInstance, AsyncInstance } from '../utils/async-instance'; +import { CoreConfig } from './config'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); @@ -84,14 +88,11 @@ export class CoreSitesProvider { protected siteSchemasMigration: { [siteId: string]: Promise } = {}; protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; protected pluginsSiteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {}; - - // Variables for DB. - protected appDB: Promise; - protected resolveAppDB!: (appDB: SQLiteDB) => void; protected siteTables: Record>> = {}; + protected schemasTables: Record>> = {}; + protected sitesTable = asyncInstance>(); constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) { - this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreSitesProvider'); this.siteSchemas = CoreArray.flatten(siteSchemas).reduce( (siteSchemas, schema) => { @@ -132,7 +133,15 @@ export class CoreSitesProvider { // Ignore errors. } - this.resolveAppDB(CoreApp.getDB()); + const sitesTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + CoreApp.getDB(), + SITES_TABLE_NAME, + ); + + await sitesTable.initialize(); + + this.sitesTable.setInstance(sitesTable); } /** @@ -743,8 +752,7 @@ export class CoreSitesProvider { config?: CoreSiteConfig, oauthId?: number, ): Promise { - const db = await this.appDB; - const entry: SiteDBEntry = { + await this.sitesTable.insert({ id, siteUrl, token, @@ -753,9 +761,7 @@ export class CoreSitesProvider { config: config ? JSON.stringify(config) : undefined, loggedOut: 0, oauthId, - }; - - await db.insertRecord(SITES_TABLE_NAME, entry); + }); } /** @@ -982,9 +988,7 @@ export class CoreSitesProvider { delete this.sites[siteId]; try { - const db = await this.appDB; - - await db.deleteRecords(SITES_TABLE_NAME, { id: siteId }); + await this.sitesTable.deleteByPrimaryKey({ id: siteId }); } catch (err) { // DB remove shouldn't fail, but we'll go ahead even if it does. } @@ -1001,10 +1005,9 @@ export class CoreSitesProvider { * @return Promise resolved with true if there are sites and false if there aren't. */ async hasSites(): Promise { - const db = await this.appDB; - const count = await db.countRecords(SITES_TABLE_NAME); + const isEmpty = await this.sitesTable.isEmpty(); - return count > 0; + return !isEmpty; } /** @@ -1026,9 +1029,8 @@ export class CoreSitesProvider { return this.sites[siteId]; } else { // Retrieve and create the site. - const db = await this.appDB; try { - const data = await db.getRecord(SITES_TABLE_NAME, { id: siteId }); + const data = await this.sitesTable.getOneByPrimaryKey({ id: siteId }); return this.makeSiteFromSiteListEntry(data); } catch { @@ -1044,8 +1046,7 @@ export class CoreSitesProvider { * @return Promise resolved with the site. */ async getSiteByUrl(siteUrl: string): Promise { - const db = await this.appDB; - const data = await db.getRecord(SITES_TABLE_NAME, { siteUrl }); + const data = await this.sitesTable.getOne({ siteUrl }); if (this.sites[data.id] !== undefined) { return this.sites[data.id]; @@ -1131,8 +1132,7 @@ export class CoreSitesProvider { * @return Promise resolved when the sites are retrieved. */ async getSites(ids?: string[]): Promise { - const db = await this.appDB; - const sites = await db.getAllRecords(SITES_TABLE_NAME); + const sites = await this.sitesTable.getMany(); const formattedSites: CoreSiteBasicInfo[] = []; sites.forEach((site) => { @@ -1197,8 +1197,7 @@ export class CoreSitesProvider { * @return Promise resolved when the sites IDs are retrieved. */ async getLoggedInSitesIds(): Promise { - const db = await this.appDB; - const sites = await db.getRecords(SITES_TABLE_NAME, { loggedOut : 0 }); + const sites = await this.sitesTable.getMany({ loggedOut : 0 }); return sites.map((site) => site.id); } @@ -1209,8 +1208,7 @@ export class CoreSitesProvider { * @return Promise resolved when the sites IDs are retrieved. */ async getSitesIds(): Promise { - const db = await this.appDB; - const sites = await db.getAllRecords(SITES_TABLE_NAME); + const sites = await this.sitesTable.getMany(); return sites.map((site) => site.id); } @@ -1233,13 +1231,7 @@ export class CoreSitesProvider { * @return Promise resolved when current site is stored. */ async login(siteId: string): Promise { - const db = await this.appDB; - const entry = { - id: 1, - siteId, - }; - - await db.insertRecord(CURRENT_SITE_TABLE_NAME, entry); + await CoreConfig.set('current_site_id', siteId); CoreEvents.trigger(CoreEvents.LOGIN, {}, siteId); } @@ -1308,13 +1300,10 @@ export class CoreSitesProvider { return Promise.reject(new CoreError('Session already restored.')); } - const db = await this.appDB; - this.sessionRestored = true; try { - const currentSite = await db.getRecord(CURRENT_SITE_TABLE_NAME, { id: 1 }); - const siteId = currentSite.siteId; + const siteId = await this.getStoredCurrentSiteId(); this.logger.debug(`Restore session in site ${siteId}`); await this.loadSite(siteId); @@ -1330,12 +1319,11 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ protected async setSiteLoggedOut(siteId: string): Promise { - const db = await this.appDB; const site = await this.getSite(siteId); site.setLoggedOut(true); - await db.updateRecords(SITES_TABLE_NAME, { loggedOut: 1 }, { id: siteId }); + await this.sitesTable.update({ loggedOut: 1 }, { id: siteId }); } /** @@ -1371,19 +1359,20 @@ export class CoreSitesProvider { * @return A promise resolved when the site is updated. */ async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise { - const db = await this.appDB; const site = await this.getSite(siteId); - const newValues: Partial = { - token, - privateToken, - loggedOut: 0, - }; site.token = token; site.privateToken = privateToken; site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore. - await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); + await this.sitesTable.update( + { + token, + privateToken, + loggedOut: 0, + }, + { id: siteId }, + ); } /** @@ -1425,9 +1414,7 @@ export class CoreSitesProvider { } try { - const db = await this.appDB; - - await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId }); + await this.sitesTable.update(newValues, { id: siteId }); } finally { CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId); } @@ -1484,8 +1471,7 @@ export class CoreSitesProvider { } try { - const db = await this.appDB; - const siteEntries = await db.getAllRecords(SITES_TABLE_NAME); + const siteEntries = await this.sitesTable.getMany(); const ids: string[] = []; const promises: Promise[] = []; @@ -1516,10 +1502,9 @@ export class CoreSitesProvider { * @return Promise resolved with the site ID. */ async getStoredCurrentSiteId(): Promise { - const db = await this.appDB; - const currentSite = await db.getRecord(CURRENT_SITE_TABLE_NAME, { id: 1 }); + await this.migrateCurrentSiteLegacyTable(); - return currentSite.siteId; + return CoreConfig.get('current_site_id'); } /** @@ -1528,9 +1513,7 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ async removeStoredCurrentSite(): Promise { - const db = await this.appDB; - - await db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 }); + await CoreConfig.delete('current_site_id'); } /** @@ -1645,10 +1628,8 @@ export class CoreSitesProvider { * @return Promise resolved when done. */ protected async applySiteSchemas(site: CoreSite, schemas: {[name: string]: CoreRegisteredSiteSchema}): Promise { - const db = site.getDb(); - // Fetch installed versions of the schema. - const records = await db.getAllRecords(SCHEMA_VERSIONS_TABLE_NAME); + const records = await this.getSiteSchemasTable(site).getMany(); const versions: {[name: string]: number} = {}; records.forEach((record) => { @@ -1695,7 +1676,7 @@ export class CoreSitesProvider { } // Set installed version. - await db.insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name: schema.name, version: schema.version }); + await this.getSiteSchemasTable(site).insert({ name: schema.name, version: schema.version }); } /** @@ -1794,6 +1775,47 @@ export class CoreSitesProvider { return []; } + /** + * Migrate the legacy current_site table. + */ + protected async migrateCurrentSiteLegacyTable(): Promise { + if (await CoreConfig.has('current_site_migrated')) { + // Already migrated. + return; + } + + try { + const db = CoreApp.getDB(); + + const { siteId } = await db.getRecord<{ siteId: string }>('current_site'); + + await CoreConfig.set('current_site_id', siteId); + await CoreApp.deleteTableSchema('current_site'); + await db.dropTable('current_site'); + } finally { + await CoreConfig.set('current_site_migrated', 1); + } + } + + /** + * Get schemas table for the given site. + * + * @param site Site. + * @returns Scehmas Table. + */ + protected getSiteSchemasTable(site: CoreSite): AsyncInstance> { + this.schemasTables[site.getId()] = this.schemasTables[site.getId()] ?? asyncInstance( + () => this.getSiteTable(SCHEMA_VERSIONS_TABLE_NAME, { + siteId: site.getId(), + database: site.getDb(), + config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + primaryKeyColumns: ['name'], + }), + ); + + return this.schemasTables[site.getId()]; + } + } export const CoreSites = makeSingleton(CoreSitesProvider); From 18c48940bf06bf1ac89093c2800e3af7bce598c0 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 10 Feb 2022 15:55:03 +0100 Subject: [PATCH 4/8] MOBILE-3981 core: Optimize cron table --- src/core/services/cron.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/core/services/cron.ts b/src/core/services/cron.ts index a5b82dfa3..eca0b1447 100644 --- a/src/core/services/cron.ts +++ b/src/core/services/cron.ts @@ -18,12 +18,14 @@ import { CoreApp } from '@services/app'; import { CoreConfig } from '@services/config'; import { CoreUtils } from '@services/utils/utils'; import { CoreConstants } from '@/core/constants'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreError } from '@classes/errors/error'; import { makeSingleton } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { APP_SCHEMA, CRON_TABLE_NAME, CronDBEntry } from '@services/database/cron'; +import { asyncInstance } from '../utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; /* * Service to handle cron processes. The registered processes will be executed every certain time. @@ -39,13 +41,9 @@ export class CoreCronDelegateService { protected logger: CoreLogger; protected handlers: { [s: string]: CoreCronHandler } = {}; protected queuePromise: Promise = Promise.resolve(); - - // Variables for DB. - protected appDB: Promise; - protected resolveAppDB!: (appDB: SQLiteDB) => void; + protected table = asyncInstance>(); constructor() { - this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CoreCronDelegate'); } @@ -59,7 +57,15 @@ export class CoreCronDelegateService { // Ignore errors. } - this.resolveAppDB(CoreApp.getDB()); + const table = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + CoreApp.getDB(), + CRON_TABLE_NAME, + ); + + await table.initialize(); + + this.table.setInstance(table); } /** @@ -238,11 +244,10 @@ export class CoreCronDelegateService { * @return Promise resolved with the handler's last execution time. */ protected async getHandlerLastExecutionTime(name: string): Promise { - const db = await this.appDB; const id = this.getHandlerLastExecutionId(name); try { - const entry = await db.getRecord(CRON_TABLE_NAME, { id }); + const entry = await this.table.getOneByPrimaryKey({ id }); const time = Number(entry.value); @@ -397,14 +402,13 @@ export class CoreCronDelegateService { * @return Promise resolved when the execution time is saved. */ protected async setHandlerLastExecutionTime(name: string, time: number): Promise { - const db = await this.appDB; const id = this.getHandlerLastExecutionId(name); const entry = { id, value: time, }; - await db.insertRecord(CRON_TABLE_NAME, entry); + await this.table.insert(entry); } /** From 84307d6f70cf07e3f661dc9257961ce070b0a9c2 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 10 Feb 2022 15:57:02 +0100 Subject: [PATCH 5/8] MOBILE-3981 pushnotifications: Optimize tables --- .../services/pushnotifications.ts | 95 +++++++++++-------- 1 file changed, 56 insertions(+), 39 deletions(-) diff --git a/src/core/features/pushnotifications/services/pushnotifications.ts b/src/core/features/pushnotifications/services/pushnotifications.ts index a4aadf0a7..be515b7c9 100644 --- a/src/core/features/pushnotifications/services/pushnotifications.ts +++ b/src/core/features/pushnotifications/services/pushnotifications.ts @@ -24,7 +24,6 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConfig } from '@services/config'; import { CoreConstants } from '@/core/constants'; -import { SQLiteDB } from '@classes/sqlitedb'; import { CoreSite, CoreSiteInfo } from '@classes/site'; import { makeSingleton, Badge, Push, Device, Translate, Platform, ApplicationInit, NgZone } from '@singletons'; import { CoreLogger } from '@singletons/logger'; @@ -42,6 +41,11 @@ import { CoreError } from '@classes/errors/error'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreSitesFactory } from '@services/sites-factory'; import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; +import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; +import { CoreObject } from '@singletons/object'; +import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; /** * Service to handle push notifications. @@ -53,14 +57,27 @@ export class CorePushNotificationsProvider { protected logger: CoreLogger; protected pushID?: string; + protected badgesTable = asyncInstance>(); + protected pendingUnregistersTable = + asyncInstance>(); - // Variables for DB. - protected appDB: Promise; - protected resolveAppDB!: (appDB: SQLiteDB) => void; + protected registeredDevicesTables: + LazyMap>>; constructor() { - this.appDB = new Promise(resolve => this.resolveAppDB = resolve); this.logger = CoreLogger.getInstance('CorePushNotificationsProvider'); + this.registeredDevicesTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable( + REGISTERED_DEVICES_TABLE_NAME, + { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + primaryKeyColumns: ['appid', 'uuid'], + }, + ), + ), + ); } /** @@ -157,7 +174,27 @@ export class CorePushNotificationsProvider { // Ignore errors. } - this.resolveAppDB(CoreApp.getDB()); + const database = CoreApp.getDB(); + const badgesTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + database, + BADGE_TABLE_NAME, + ['siteid', 'addon'], + ); + const pendingUnregistersTable = new CoreDatabaseTableProxy( + { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + database, + PENDING_UNREGISTER_TABLE_NAME, + ['siteid'], + ); + + await Promise.all([ + badgesTable.initialize(), + pendingUnregistersTable.initialize(), + ]); + + this.badgesTable.setInstance(badgesTable); + this.pendingUnregistersTable.setInstance(pendingUnregistersTable); } /** @@ -177,8 +214,7 @@ export class CorePushNotificationsProvider { */ async cleanSiteCounters(siteId: string): Promise { try { - const db = await this.appDB; - await db.deleteRecords(BADGE_TABLE_NAME, { siteid: siteId } ); + await this.badgesTable.delete({ siteid: siteId }); } finally { this.updateAppCounter(); } @@ -514,7 +550,6 @@ export class CorePushNotificationsProvider { this.logger.debug(`Unregister device on Moodle: '${site.getId()}'`); - const db = await this.appDB; const data: CoreUserRemoveUserDeviceWSParams = { appid: CoreConstants.CONFIG.app_id, uuid: Device.uuid, @@ -526,7 +561,7 @@ export class CorePushNotificationsProvider { } catch (error) { if (CoreUtils.isWebServiceError(error) || CoreUtils.isExpiredTokenError(error)) { // Cannot unregister. Don't try again. - await CoreUtils.ignoreErrors(db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, { + await CoreUtils.ignoreErrors(this.pendingUnregistersTable.delete({ token: site.getToken(), siteid: site.getId(), })); @@ -535,13 +570,12 @@ export class CorePushNotificationsProvider { } // Store the pending unregister so it's retried again later. - const entry: CorePushNotificationsPendingUnregisterDBRecord = { + await this.pendingUnregistersTable.insert({ siteid: site.getId(), siteurl: site.getURL(), token: site.getToken(), info: JSON.stringify(site.getInfo()), - }; - await db.insertRecord(PENDING_UNREGISTER_TABLE_NAME, entry); + }); return; } @@ -552,9 +586,9 @@ export class CorePushNotificationsProvider { await CoreUtils.ignoreErrors(Promise.all([ // Remove the device from the local DB. - site.getDb().deleteRecords(REGISTERED_DEVICES_TABLE_NAME, this.getRegisterData()), + this.registeredDevicesTables[site.getId()].delete(this.getRegisterData()), // Remove pending unregisters for this site. - db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, { siteid: site.getId() }), + this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() }), ])); } @@ -714,15 +748,14 @@ export class CorePushNotificationsProvider { // Insert the device in the local DB. try { - await site.getDb().insertRecord(REGISTERED_DEVICES_TABLE_NAME, data); + await this.registeredDevicesTables[site.getId()].insert(data); } catch (err) { // Ignore errors. } } } finally { // Remove pending unregisters for this site. - const db = await this.appDB; - await CoreUtils.ignoreErrors(db.deleteRecords(PENDING_UNREGISTER_TABLE_NAME, { siteid: site.getId() })); + await CoreUtils.ignoreErrors(this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() })); } } @@ -735,8 +768,7 @@ export class CorePushNotificationsProvider { */ protected async getAddonBadge(siteId?: string, addon: string = 'site'): Promise { try { - const db = await this.appDB; - const entry = await db.getRecord(BADGE_TABLE_NAME, { siteid: siteId, addon }); + const entry = await this.badgesTable.getOne({ siteid: siteId, addon }); return entry?.number || 0; } catch (err) { @@ -751,19 +783,7 @@ export class CorePushNotificationsProvider { * @return Promise resolved when done. */ async retryUnregisters(siteId?: string): Promise { - - const db = await this.appDB; - let results: CorePushNotificationsPendingUnregisterDBRecord[]; - - if (siteId) { - // Check if the site has a pending unregister. - results = await db.getRecords(PENDING_UNREGISTER_TABLE_NAME, { - siteid: siteId, - }); - } else { - // Get all pending unregisters. - results = await db.getAllRecords(PENDING_UNREGISTER_TABLE_NAME); - } + const results = await this.pendingUnregistersTable.getMany(CoreObject.withoutEmpty({ siteid: siteId })); await Promise.all(results.map(async (result) => { // Create a temporary site to unregister. @@ -789,14 +809,11 @@ export class CorePushNotificationsProvider { protected async saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); - const entry: CorePushNotificationsBadgeDBRecord = { + await this.badgesTable.insert({ siteid: siteId, addon, number: value, // eslint-disable-line id-blacklist - }; - - const db = await this.appDB; - await db.insertRecord(BADGE_TABLE_NAME, entry); + }); return value; } @@ -815,7 +832,7 @@ export class CorePushNotificationsProvider { // Check if the device is already registered. const records = await CoreUtils.ignoreErrors( - site.getDb().getRecords(REGISTERED_DEVICES_TABLE_NAME, { + this.registeredDevicesTables[site.getId()].getMany({ appid: data.appid, uuid: data.uuid, name: data.name, From 4504d1ba8ab0d44b1eb59653b67d9f14a770c0cb Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 10 Feb 2022 15:59:22 +0100 Subject: [PATCH 6/8] MOBILE-3981 course: Optimize tables --- src/core/features/course/services/course.ts | 41 ++++++++++++--------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 565198a71..5e1496358 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -45,6 +45,10 @@ import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; import { CoreTagItem } from '@features/tag/services/tag'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreCourseModuleDelegate } from './module-delegate'; +import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; +import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -140,9 +144,18 @@ export class CoreCourseProvider { ]; protected logger: CoreLogger; + protected statusTables: LazyMap>>; constructor() { this.logger = CoreLogger.getInstance('CoreCourseProvider'); + this.statusTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(COURSE_STATUS_TABLE, { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + }), + ), + ); } /** @@ -221,7 +234,7 @@ export class CoreCourseProvider { const site = await CoreSites.getSite(siteId); this.logger.debug('Clear all course status for site ' + site.id); - await site.getDb().deleteRecords(COURSE_STATUS_TABLE); + await this.statusTables[site.getId()].delete(); this.triggerCourseStatusChanged(CoreCourseProvider.ALL_COURSES_CLEARED, CoreConstants.NOT_DOWNLOADED, site.id); } @@ -373,7 +386,7 @@ export class CoreCourseProvider { */ async getCourseStatusData(courseId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); - const entry: CoreCourseStatusDBRecord = await site.getDb().getRecord(COURSE_STATUS_TABLE, { id: courseId }); + const entry = await this.statusTables[site.getId()].getOneByPrimaryKey({ id: courseId }); if (!entry) { throw Error('No entry found on course status table'); } @@ -405,16 +418,13 @@ export class CoreCourseProvider { * @return Resolves with an array containing downloaded course ids. */ async getDownloadedCourseIds(siteId?: string): Promise { + const downloadedStatuses = [CoreConstants.DOWNLOADED, CoreConstants.DOWNLOADING, CoreConstants.OUTDATED]; const site = await CoreSites.getSite(siteId); - const entries: CoreCourseStatusDBRecord[] = await site.getDb().getRecordsList( - COURSE_STATUS_TABLE, - 'status', - [ - CoreConstants.DOWNLOADED, - CoreConstants.DOWNLOADING, - CoreConstants.OUTDATED, - ], - ); + const entries = await this.statusTables[site.getId()].getManyWhere({ + sql: 'status IN (?,?,?)', + sqlParams: downloadedStatuses, + js: ({ status }) => downloadedStatuses.includes(status), + }); return entries.map((entry) => entry.id); } @@ -1269,7 +1279,6 @@ export class CoreCourseProvider { this.logger.debug(`Set previous status for course ${courseId} in site ${siteId}`); const site = await CoreSites.getSite(siteId); - const db = site.getDb(); const entry = await this.getCourseStatusData(courseId, siteId); this.logger.debug(`Set previous status '${entry.status}' for course ${courseId}`); @@ -1282,7 +1291,7 @@ export class CoreCourseProvider { downloadTime: entry.status == CoreConstants.DOWNLOADING ? entry.previousDownloadTime : entry.downloadTime, }; - await db.updateRecords(COURSE_STATUS_TABLE, newData, { id: courseId }); + await this.statusTables[site.getId()].update(newData, { id: courseId }); // Success updating, trigger event. this.triggerCourseStatusChanged(courseId, newData.status, siteId); @@ -1329,16 +1338,14 @@ export class CoreCourseProvider { if (previousStatus != status) { // Status has changed, update it. - const data: CoreCourseStatusDBRecord = { + await this.statusTables[site.getId()].insert({ id: courseId, status: status, previous: previousStatus, updated: new Date().getTime(), downloadTime: downloadTime, previousDownloadTime: previousDownloadTime, - }; - - await site.getDb().insertRecord(COURSE_STATUS_TABLE, data); + }); } // Success inserting, trigger event. From b95c67874f622aab929bf4844070266356a58542 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 10 Feb 2022 15:59:54 +0100 Subject: [PATCH 7/8] MOBILE-3981 core: Optimize site tables --- src/core/classes/site.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/classes/site.ts b/src/core/classes/site.ts index 0c91b0466..f88defc5d 100644 --- a/src/core/classes/site.ts +++ b/src/core/classes/site.ts @@ -108,6 +108,7 @@ export class CoreSite { protected logger: CoreLogger; protected db?: SQLiteDB; protected cacheTable: AsyncInstance>; + protected configTable: AsyncInstance>; protected cleanUnicode = false; protected lastAutoLogin = 0; protected offlineDisabled = false; @@ -146,6 +147,12 @@ export class CoreSite { database: this.getDb(), config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, })); + this.configTable = asyncInstance(() => CoreSites.getSiteTable(CoreSite.CONFIG_TABLE, { + siteId: this.getId(), + database: this.getDb(), + config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, + primaryKeyColumns: ['name'], + })); this.setInfo(infos); this.calculateOfflineDisabled(); @@ -1825,7 +1832,7 @@ export class CoreSite { * @return Promise resolved when done. */ async deleteSiteConfig(name: string): Promise { - await this.getDb().deleteRecords(CoreSite.CONFIG_TABLE, { name }); + await this.configTable.deleteByPrimaryKey({ name }); } /** @@ -1837,7 +1844,7 @@ export class CoreSite { */ async getLocalSiteConfig(name: string, defaultValue?: T): Promise { try { - const entry = await this.getDb().getRecord(CoreSite.CONFIG_TABLE, { name }); + const entry = await this.configTable.getOneByPrimaryKey({ name }); return entry.value; } catch (error) { @@ -1857,7 +1864,7 @@ export class CoreSite { * @return Promise resolved when done. */ async setLocalSiteConfig(name: string, value: number | string): Promise { - await this.getDb().insertRecord(CoreSite.CONFIG_TABLE, { name, value }); + await this.configTable.insert({ name, value }); } /** From 95e566a9c29634a8eec59c91e43245abedbf9b6e Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Thu, 10 Feb 2022 16:03:00 +0100 Subject: [PATCH 8/8] MOBILE-3981 core: Log database table instance --- src/core/classes/database/debug-database-table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/classes/database/debug-database-table.ts b/src/core/classes/database/debug-database-table.ts index f944b8a01..3e236c5c3 100644 --- a/src/core/classes/database/debug-database-table.ts +++ b/src/core/classes/database/debug-database-table.ts @@ -47,7 +47,7 @@ export class CoreDebugDatabaseTable< * @inheritdoc */ initialize(): Promise { - this.logger.log('initialize'); + this.logger.log('initialize', this.target); return this.target.initialize(); }