Merge pull request #3127 from NoelDeMartin/MOBILE-3821

MOBILE-3821: Database optimization final tweaks (before 4.0)
main
Dani Palou 2022-02-21 12:51:12 +01:00 committed by GitHub
commit 5b0abf40f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 282 additions and 123 deletions

View File

@ -54,18 +54,21 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
protected unreadCount = 0; protected unreadCount = 0;
protected contactRequestsCount = 0; protected contactRequestsCount = 0;
protected orMore = false; protected orMore = false;
protected badgeCount?: number;
constructor() { constructor() {
CoreEvents.on(AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, (data) => { CoreEvents.on(AddonMessagesProvider.UNREAD_CONVERSATION_COUNTS_EVENT, (data) => {
this.unreadCount = data.favourites + data.individual + data.group + data.self; this.unreadCount = data.favourites + data.individual + data.group + data.self;
this.orMore = !!data.orMore; this.orMore = !!data.orMore;
this.updateBadge(data.siteId!);
data.siteId && this.updateBadge(data.siteId);
}); });
CoreEvents.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => { CoreEvents.on(AddonMessagesProvider.CONTACT_REQUESTS_COUNT_EVENT, (data) => {
this.contactRequestsCount = data.count; this.contactRequestsCount = data.count;
this.updateBadge(data.siteId!);
data.siteId && this.updateBadge(data.siteId);
}); });
// Reset info on logout. // Reset info on logout.
@ -123,27 +126,28 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
* @return Resolve when done. * @return Resolve when done.
*/ */
async refreshBadge(siteId?: string, unreadOnly?: boolean): Promise<void> { async refreshBadge(siteId?: string, unreadOnly?: boolean): Promise<void> {
siteId = siteId || CoreSites.getCurrentSiteId(); const badgeSiteId = siteId || CoreSites.getCurrentSiteId();
if (!siteId) {
if (!badgeSiteId) {
return; return;
} }
const promises: Promise<unknown>[] = []; const promises: Promise<unknown>[] = [];
promises.push(AddonMessages.refreshUnreadConversationCounts(siteId).catch(() => { promises.push(AddonMessages.refreshUnreadConversationCounts(badgeSiteId).catch(() => {
this.unreadCount = 0; this.unreadCount = 0;
this.orMore = false; this.orMore = false;
})); }));
// Refresh the number of contact requests in 3.6+ sites. // Refresh the number of contact requests in 3.6+ sites.
if (!unreadOnly && AddonMessages.isGroupMessagingEnabled()) { if (!unreadOnly && AddonMessages.isGroupMessagingEnabled()) {
promises.push(AddonMessages.refreshContactRequestsCount(siteId).catch(() => { promises.push(AddonMessages.refreshContactRequestsCount(badgeSiteId).catch(() => {
this.contactRequestsCount = 0; this.contactRequestsCount = 0;
})); }));
} }
await Promise.all(promises).finally(() => { await Promise.all(promises).finally(() => {
this.updateBadge(siteId!); this.updateBadge(badgeSiteId);
this.handler.loading = false; this.handler.loading = false;
}); });
} }
@ -155,6 +159,13 @@ export class AddonMessagesMainMenuHandlerService implements CoreMainMenuHandler,
*/ */
updateBadge(siteId: string): void { updateBadge(siteId: string): void {
const totalCount = this.unreadCount + (this.contactRequestsCount || 0); const totalCount = this.unreadCount + (this.contactRequestsCount || 0);
if (this.badgeCount === totalCount) {
return;
}
this.badgeCount = totalCount;
if (totalCount > 0) { if (totalCount > 0) {
this.handler.badge = totalCount + (this.orMore ? '+' : ''); this.handler.badge = totalCount + (this.orMore ? '+' : '');
} else { } else {

View File

@ -69,6 +69,7 @@ export class AddonModBookProvider {
() => CoreSites.getSiteTable(LAST_CHAPTER_VIEWED_TABLE, { () => CoreSites.getSiteTable(LAST_CHAPTER_VIEWED_TABLE, {
siteId, siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
onDestroy: () => delete this.lastChapterViewedTables[siteId],
}), }),
), ),
); );

View File

@ -14,10 +14,11 @@
import { CoreConstants } from '@/core/constants'; import { CoreConstants } from '@/core/constants';
import { asyncInstance } from '@/core/utils/async-instance'; import { asyncInstance } from '@/core/utils/async-instance';
import { SQLiteDB, SQLiteDBRecordValues } from '@classes/sqlitedb'; import { SQLiteDBRecordValues } from '@classes/sqlitedb';
import { CoreConfig, CoreConfigProvider } from '@services/config'; import { CoreConfig, CoreConfigProvider } from '@services/config';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { import {
CoreDatabaseConfiguration,
CoreDatabaseReducer, CoreDatabaseReducer,
CoreDatabaseTable, CoreDatabaseTable,
CoreDatabaseConditions, CoreDatabaseConditions,
@ -40,7 +41,8 @@ export class CoreDatabaseTableProxy<
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> { > extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
protected config: CoreDatabaseConfiguration; protected readonly DEFAULT_CACHING_STRATEGY = CoreDatabaseCachingStrategy.None;
protected target = asyncInstance<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>(); protected target = asyncInstance<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>>();
protected environmentObserver?: CoreEventObserver; protected environmentObserver?: CoreEventObserver;
protected targetConstructors: Record< protected targetConstructors: Record<
@ -52,21 +54,12 @@ export class CoreDatabaseTableProxy<
[CoreDatabaseCachingStrategy.None]: CoreDatabaseTable, [CoreDatabaseCachingStrategy.None]: CoreDatabaseTable,
}; };
constructor(
config: Partial<CoreDatabaseConfiguration>,
database: SQLiteDB,
tableName: string,
primaryKeyColumns?: PrimaryKeyColumn[],
) {
super(database, tableName, primaryKeyColumns);
this.config = { ...this.getConfigDefaults(), ...config };
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
async initialize(): Promise<void> { async initialize(): Promise<void> {
await super.initialize();
this.environmentObserver = CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, async () => { this.environmentObserver = CoreEvents.on(CoreConfigProvider.ENVIRONMENT_UPDATED, async () => {
if (!(await this.shouldUpdateTarget())) { if (!(await this.shouldUpdateTarget())) {
return; return;
@ -82,9 +75,23 @@ export class CoreDatabaseTableProxy<
* @inheritdoc * @inheritdoc
*/ */
async destroy(): Promise<void> { async destroy(): Promise<void> {
await super.destroy();
this.environmentObserver?.off(); this.environmentObserver?.off();
} }
/**
* @inheritdoc
*/
matchesConfig(config: Partial<CoreDatabaseConfiguration>): boolean {
const thisDebug = this.config.debug ?? false;
const thisCachingStrategy = this.config.cachingStrategy ?? this.DEFAULT_CACHING_STRATEGY;
const otherDebug = config.debug ?? false;
const otherCachingStrategy = config.cachingStrategy ?? this.DEFAULT_CACHING_STRATEGY;
return super.matchesConfig(config) && thisDebug === otherDebug && thisCachingStrategy === otherCachingStrategy;
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -172,24 +179,12 @@ export class CoreDatabaseTableProxy<
return this.target.deleteByPrimaryKey(primaryKey); return this.target.deleteByPrimaryKey(primaryKey);
} }
/**
* Get default configuration values.
*
* @returns Config defaults.
*/
protected getConfigDefaults(): CoreDatabaseConfiguration {
return {
cachingStrategy: CoreDatabaseCachingStrategy.None,
debug: false,
};
}
/** /**
* Get database configuration to use at runtime. * Get database configuration to use at runtime.
* *
* @returns Database configuration. * @returns Database configuration.
*/ */
protected async getRuntimeConfig(): Promise<CoreDatabaseConfiguration> { protected async getRuntimeConfig(): Promise<Partial<CoreDatabaseConfiguration>> {
await CoreConfig.ready(); await CoreConfig.ready();
return { return {
@ -228,7 +223,8 @@ export class CoreDatabaseTableProxy<
const originalTarget = target instanceof CoreDebugDatabaseTable ? target.getTarget() : target; const originalTarget = target instanceof CoreDebugDatabaseTable ? target.getTarget() : target;
return (config.debug && target === originalTarget) return (config.debug && target === originalTarget)
|| originalTarget?.constructor !== this.targetConstructors[config.cachingStrategy]; || originalTarget?.constructor !== this.targetConstructors[config.cachingStrategy ?? this.DEFAULT_CACHING_STRATEGY]
|| !originalTarget.matchesConfig(config);
} }
/** /**
@ -238,7 +234,7 @@ export class CoreDatabaseTableProxy<
*/ */
protected async createTarget(): Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> { protected async createTarget(): Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> {
const config = await this.getRuntimeConfig(); const config = await this.getRuntimeConfig();
const table = this.createTable(config.cachingStrategy); const table = this.createTable(config);
return config.debug ? new CoreDebugDatabaseTable(table) : table; return config.debug ? new CoreDebugDatabaseTable(table) : table;
} }
@ -246,25 +242,31 @@ export class CoreDatabaseTableProxy<
/** /**
* Create a database table using the given caching strategy. * Create a database table using the given caching strategy.
* *
* @param cachingStrategy Caching strategy. * @param config Database configuration.
* @returns Database table. * @returns Database table.
*/ */
protected createTable(cachingStrategy: CoreDatabaseCachingStrategy): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> { protected createTable(config: Partial<CoreDatabaseConfiguration>): CoreDatabaseTable<DBRecord, PrimaryKeyColumn> {
const DatabaseTable = this.targetConstructors[cachingStrategy]; const DatabaseTable = this.targetConstructors[config.cachingStrategy ?? this.DEFAULT_CACHING_STRATEGY];
return new DatabaseTable(this.database, this.tableName, this.primaryKeyColumns); return new DatabaseTable(config, this.database, this.tableName, this.primaryKeyColumns);
} }
} }
declare module '@classes/database/database-table' {
/** /**
* Database proxy configuration. * Augment CoreDatabaseConfiguration interface with data specific to this class.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/ */
export interface CoreDatabaseConfiguration { export interface CoreDatabaseConfiguration {
cachingStrategy: CoreDatabaseCachingStrategy; cachingStrategy: CoreDatabaseCachingStrategy;
debug: boolean; debug: boolean;
} }
}
/** /**
* Database caching strategies. * Database caching strategies.
*/ */

View File

@ -24,16 +24,31 @@ export class CoreDatabaseTable<
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
> { > {
protected config: Partial<CoreDatabaseConfiguration>;
protected database: SQLiteDB; protected database: SQLiteDB;
protected tableName: string; protected tableName: string;
protected primaryKeyColumns: PrimaryKeyColumn[]; protected primaryKeyColumns: PrimaryKeyColumn[];
protected listeners: CoreDatabaseTableListener[] = [];
constructor(database: SQLiteDB, tableName: string, primaryKeyColumns?: PrimaryKeyColumn[]) { constructor(
config: Partial<CoreDatabaseConfiguration>,
database: SQLiteDB,
tableName: string,
primaryKeyColumns?: PrimaryKeyColumn[],
) {
this.config = config;
this.database = database; this.database = database;
this.tableName = tableName; this.tableName = tableName;
this.primaryKeyColumns = primaryKeyColumns ?? ['id'] as PrimaryKeyColumn[]; this.primaryKeyColumns = primaryKeyColumns ?? ['id'] as PrimaryKeyColumn[];
} }
/**
* Get database configuration.
*/
getConfig(): Partial<CoreDatabaseConfiguration> {
return this.config;
}
/** /**
* Get database connection. * Get database connection.
* *
@ -72,7 +87,27 @@ export class CoreDatabaseTable<
* Destroy. * Destroy.
*/ */
async destroy(): Promise<void> { async destroy(): Promise<void> {
// Nothing to destroy by default, override this method if necessary. this.listeners.forEach(listener => listener.onDestroy?.());
}
/**
* Add listener.
*
* @param listener Listener.
*/
addListener(listener: CoreDatabaseTableListener): void {
this.listeners.push(listener);
}
/**
* Check whether the table matches the given configuration for the values that concern it.
*
* @param config Database config.
* @returns Whether the table matches the given configuration.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
matchesConfig(config: Partial<CoreDatabaseConfiguration>): boolean {
return true;
} }
/** /**
@ -336,6 +371,20 @@ export class CoreDatabaseTable<
} }
/**
* Database configuration.
*/
export interface CoreDatabaseConfiguration {
// This definition is augmented in subclasses.
}
/**
* Database table listener.
*/
export interface CoreDatabaseTableListener {
onDestroy?(): void;
}
/** /**
* CoreDatabaseTable constructor. * CoreDatabaseTable constructor.
*/ */
@ -346,6 +395,7 @@ export type CoreDatabaseTableConstructor<
> = { > = {
new ( new (
config: Partial<CoreDatabaseConfiguration>,
database: SQLiteDB, database: SQLiteDB,
tableName: string, tableName: string,
primaryKeyColumns?: PrimaryKeyColumn[] primaryKeyColumns?: PrimaryKeyColumn[]

View File

@ -37,7 +37,7 @@ export class CoreDebugDatabaseTable<
protected logger: CoreLogger; protected logger: CoreLogger;
constructor(target: CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey>) { constructor(target: CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey>) {
super(target.getDatabase(), target.getTableName(), target.getPrimaryKeyColumns()); super(target.getConfig(), target.getDatabase(), target.getTableName(), target.getPrimaryKeyColumns());
this.target = target; this.target = target;
this.logger = CoreLogger.getInstance(`CoreDatabase[${this.tableName}]`); this.logger = CoreLogger.getInstance(`CoreDatabase[${this.tableName}]`);
@ -53,7 +53,9 @@ export class CoreDebugDatabaseTable<
/** /**
* @inheritdoc * @inheritdoc
*/ */
initialize(): Promise<void> { async initialize(): Promise<void> {
await super.initialize();
this.logger.log('initialize', this.target); this.logger.log('initialize', this.target);
return this.target.initialize(); return this.target.initialize();
@ -62,7 +64,9 @@ export class CoreDebugDatabaseTable<
/** /**
* @inheritdoc * @inheritdoc
*/ */
destroy(): Promise<void> { async destroy(): Promise<void> {
await super.destroy();
this.logger.log('destroy'); this.logger.log('destroy');
return this.target.destroy(); return this.target.destroy();

View File

@ -40,6 +40,8 @@ export class CoreEagerDatabaseTable<
* @inheritdoc * @inheritdoc
*/ */
async initialize(): Promise<void> { async initialize(): Promise<void> {
await super.initialize();
const records = await super.getMany(); const records = await super.getMany();
this.records = records.reduce((data, record) => { this.records = records.reduce((data, record) => {

View File

@ -14,7 +14,13 @@
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { SQLiteDBRecordValues } from '@classes/sqlitedb'; import { SQLiteDBRecordValues } from '@classes/sqlitedb';
import { CoreDatabaseTable, CoreDatabaseConditions, GetDBRecordPrimaryKey, CoreDatabaseQueryOptions } from './database-table'; import {
CoreDatabaseConfiguration,
CoreDatabaseTable,
CoreDatabaseConditions,
GetDBRecordPrimaryKey,
CoreDatabaseQueryOptions,
} from './database-table';
/** /**
* Wrapper used to improve performance by caching records that are used often for faster read operations. * Wrapper used to improve performance by caching records that are used often for faster read operations.
@ -28,7 +34,38 @@ export class CoreLazyDatabaseTable<
PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> PrimaryKey extends GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn> = GetDBRecordPrimaryKey<DBRecord, PrimaryKeyColumn>
> extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> { > extends CoreDatabaseTable<DBRecord, PrimaryKeyColumn, PrimaryKey> {
protected readonly DEFAULT_CACHE_LIFETIME = 60000;
protected records: Record<string, DBRecord | null> = {}; protected records: Record<string, DBRecord | null> = {};
protected interval?: number;
/**
* @inheritdoc
*/
async initialize(): Promise<void> {
await super.initialize();
this.interval = window.setInterval(() => (this.records = {}), this.config.lazyCacheLifetime ?? this.DEFAULT_CACHE_LIFETIME);
}
/**
* @inheritdoc
*/
async destroy(): Promise<void> {
await super.destroy();
this.interval && window.clearInterval(this.interval);
}
/**
* @inheritdoc
*/
matchesConfig(config: Partial<CoreDatabaseConfiguration>): boolean {
const thisCacheLifetime = this.config.lazyCacheLifetime ?? this.DEFAULT_CACHE_LIFETIME;
const otherCacheLifetime = config.lazyCacheLifetime ?? this.DEFAULT_CACHE_LIFETIME;
return super.matchesConfig(config) && thisCacheLifetime === otherCacheLifetime;
}
/** /**
* @inheritdoc * @inheritdoc
@ -152,3 +189,16 @@ export class CoreLazyDatabaseTable<
} }
} }
declare module '@classes/database/database-table' {
/**
* Augment CoreDatabaseConfiguration interface with data specific to this table.
*
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
export interface CoreDatabaseConfiguration {
lazyCacheLifetime: number;
}
}

View File

@ -1170,27 +1170,54 @@ export class SQLiteDB {
*/ */
protected getDatabaseSpies(db: SQLiteObject): Partial<SQLiteObject> { protected getDatabaseSpies(db: SQLiteObject): Partial<SQLiteObject> {
return { return {
executeSql(statement, params) { async executeSql(statement, params) {
const start = performance.now(); const start = performance.now();
return db.executeSql(statement, params).then(result => { try {
CoreDB.logQuery(statement, performance.now() - start, params); const result = await db.executeSql(statement, params);
CoreDB.logQuery({
params,
sql: statement,
duration: performance.now() - start,
});
return result; return result;
} catch (error) {
CoreDB.logQuery({
params,
error,
sql: statement,
duration: performance.now() - start,
}); });
},
sqlBatch(statements) {
const start = performance.now();
return db.sqlBatch(statements).then(result => { throw error;
}
},
async sqlBatch(statements) {
const start = performance.now();
const sql = Array.isArray(statements) const sql = Array.isArray(statements)
? statements.join(' | ') ? statements.join(' | ')
: String(statements); : String(statements);
CoreDB.logQuery(sql, performance.now() - start); try {
const result = await db.sqlBatch(statements);
CoreDB.logQuery({
sql,
duration: performance.now() - start,
});
return result; return result;
} catch (error) {
CoreDB.logQuery({
sql,
error,
duration: performance.now() - start,
}); });
throw error;
}
}, },
}; };
} }

View File

@ -13,12 +13,8 @@
// limitations under the License. // limitations under the License.
import { mock, mockSingleton } from '@/testing/utils'; import { mock, mockSingleton } from '@/testing/utils';
import { CoreDatabaseSorting, CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseConfiguration, CoreDatabaseSorting, CoreDatabaseTable } from '@classes/database/database-table';
import { import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
CoreDatabaseCachingStrategy,
CoreDatabaseConfiguration,
CoreDatabaseTableProxy,
} from '@classes/database/database-table-proxy';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';

View File

@ -148,6 +148,7 @@ export class CoreCourseProvider {
() => CoreSites.getSiteTable(COURSE_STATUS_TABLE, { () => CoreSites.getSiteTable(COURSE_STATUS_TABLE, {
siteId, siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
onDestroy: () => delete this.statusTables[siteId],
}), }),
), ),
); );

View File

@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
/* tslint:disable:no-console */
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { DbTransaction, SQLiteObject } from '@ionic-native/sqlite/ngx'; import { DbTransaction, SQLiteObject } from '@ionic-native/sqlite/ngx';
import { CoreDB } from '@services/db'; import { CoreDB } from '@services/db';
@ -53,7 +51,7 @@ export class SQLiteDBMock extends SQLiteDB {
await this.ready(); await this.ready();
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
this.db!.transaction((tx) => { this.db?.transaction((tx) => {
// Query all tables from sqlite_master that we have created and can modify. // Query all tables from sqlite_master that we have created and can modify.
const args = []; const args = [];
const query = `SELECT * FROM sqlite_master const query = `SELECT * FROM sqlite_master
@ -99,15 +97,13 @@ export class SQLiteDBMock extends SQLiteDB {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
// With WebSQL, all queries must be run in a transaction. // With WebSQL, all queries must be run in a transaction.
this.db!.transaction((tx) => { this.db?.transaction((tx) => {
tx.executeSql(sql, params, (tx, results) => { tx.executeSql(
resolve(results); sql,
}, (tx, error) => { params,
// eslint-disable-next-line no-console (_, results) => resolve(results),
console.error(sql, params, error); (_, error) => reject(new Error(`SQL failed: ${sql}, reason: ${error?.message}`)),
);
reject(error);
});
}); });
}); });
} }
@ -126,7 +122,7 @@ export class SQLiteDBMock extends SQLiteDB {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
// Create a transaction to execute the queries. // Create a transaction to execute the queries.
this.db!.transaction((tx) => { this.db?.transaction((tx) => {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
// Execute all the queries. Each statement can be a string or an array. // Execute all the queries. Each statement can be a string or an array.
@ -143,14 +139,7 @@ export class SQLiteDBMock extends SQLiteDB {
params = null; params = null;
} }
tx.executeSql(query, params, (tx, results) => { tx.executeSql(query, params, (_, results) => resolve(results), (_, error) => reject(error));
resolve(results);
}, (tx, error) => {
// eslint-disable-next-line no-console
console.error(query, params, error);
reject(error);
});
})); }));
}); });
@ -187,13 +176,30 @@ export class SQLiteDBMock extends SQLiteDB {
const transactionSpy: DbTransaction = { const transactionSpy: DbTransaction = {
executeSql(sql, params, success, error) { executeSql(sql, params, success, error) {
const start = performance.now(); const start = performance.now();
const resolve = callback => (...args) => {
CoreDB.logQuery(sql, performance.now() - start, params);
return callback(...args); return transaction.executeSql(
}; sql,
params,
(...args) => {
CoreDB.logQuery({
sql,
params,
duration: performance.now() - start,
});
return transaction.executeSql(sql, params, resolve(success), resolve(error)); return success?.(...args);
},
(...args) => {
CoreDB.logQuery({
sql,
params,
error: args[0],
duration: performance.now() - start,
});
return error?.(...args);
},
);
}, },
}; };

View File

@ -74,6 +74,7 @@ export class CorePushNotificationsProvider {
siteId, siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
primaryKeyColumns: ['appid', 'uuid'], primaryKeyColumns: ['appid', 'uuid'],
onDestroy: () => delete this.registeredDevicesTables[siteId],
}, },
), ),
), ),

View File

@ -13,14 +13,20 @@
// limitations under the License. // limitations under the License.
import { CoreConfig, CoreConfigProvider } from '@services/config'; import { CoreConfig, CoreConfigProvider } from '@services/config';
import { CoreDB, CoreDbProvider } from '@services/db';
import { CoreCustomURLSchemes, CoreCustomURLSchemesProvider } from '@services/urlschemes';
import { CoreConstants } from '../constants'; import { CoreConstants } from '../constants';
type DevelopmentWindow = Window & { type DevelopmentWindow = Window & {
configProvider?: CoreConfigProvider; configProvider?: CoreConfigProvider;
dbProvider?: CoreDbProvider;
urlSchemes?: CoreCustomURLSchemesProvider;
}; };
function initializeDevelopmentWindow(window: DevelopmentWindow) { function initializeDevelopmentWindow(window: DevelopmentWindow) {
window.configProvider = CoreConfig.instance; window.configProvider = CoreConfig.instance;
window.dbProvider = CoreDB.instance;
window.urlSchemes = CoreCustomURLSchemes.instance;
} }
export default function(): void { export default function(): void {

View File

@ -18,6 +18,7 @@ import { SQLiteDB } from '@classes/sqlitedb';
import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb'; import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb';
import { makeSingleton, SQLite, Platform } from '@singletons'; import { makeSingleton, SQLite, Platform } from '@singletons';
import { CoreAppProvider } from './app'; import { CoreAppProvider } from './app';
import { CoreUtils } from './utils/utils';
/** /**
* This service allows interacting with the local database to store and retrieve data. * This service allows interacting with the local database to store and retrieve data.
@ -35,15 +36,21 @@ export class CoreDbProvider {
* @returns Whether queries should be logged. * @returns Whether queries should be logged.
*/ */
loggingEnabled(): boolean { loggingEnabled(): boolean {
return CoreAppProvider.isAutomated(); return CoreUtils.hasCookie('MoodleAppDBLoggingEnabled') || CoreAppProvider.isAutomated();
} }
/** /**
* Print query history in console. * Print query history in console.
*
* @param format Log format, with the following substitutions: :sql, :duration, and :result.
*/ */
printHistory(): void { printHistory(format: string = ':sql | Duration: :duration | Result: :result'): void {
const substituteParams = ({ sql, params }: CoreDbQueryLog) => const substituteParams = ({ sql, params, duration, error }: CoreDbQueryLog) => format
Object.values(params ?? []).reduce((sql: string, param: string) => sql.replace('?', param), sql); .replace(':sql', Object
.values(params ?? [])
.reduce((sql: string, param: string) => sql.replace('?', param) as string, sql) as string)
.replace(':duration', `${Math.round(duration).toString().padStart(4, '0')}ms`)
.replace(':result', error?.message ?? 'Success');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(this.queryLogs.map(substituteParams).join('\n')); console.log(this.queryLogs.map(substituteParams).join('\n'));
@ -52,11 +59,10 @@ export class CoreDbProvider {
/** /**
* Log a query. * Log a query.
* *
* @param sql Query SQL. * @param log Query log.
* @param params Query parameters.
*/ */
logQuery(sql: string, duration: number, params?: unknown[]): void { logQuery(log: CoreDbQueryLog): void {
this.queryLogs.push({ sql, duration, params }); this.queryLogs.push(log);
} }
/** /**
@ -121,5 +127,6 @@ export const CoreDB = makeSingleton(CoreDbProvider);
export interface CoreDbQueryLog { export interface CoreDbQueryLog {
sql: string; sql: string;
duration: number; duration: number;
error?: Error;
params?: unknown[]; params?: unknown[];
} }

View File

@ -113,6 +113,7 @@ export class CoreFilepoolProvider {
siteId, siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
primaryKeyColumns: ['fileId'], primaryKeyColumns: ['fileId'],
onDestroy: () => delete this.filesTables[siteId],
}), }),
), ),
); );
@ -122,6 +123,7 @@ export class CoreFilepoolProvider {
siteId, siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
primaryKeyColumns: ['fileId', 'component', 'componentId'], primaryKeyColumns: ['fileId', 'component', 'componentId'],
onDestroy: () => delete this.linksTables[siteId],
}), }),
), ),
); );
@ -130,6 +132,7 @@ export class CoreFilepoolProvider {
() => CoreSites.getSiteTable<CoreFilepoolPackageEntry, 'id'>(PACKAGES_TABLE_NAME, { () => CoreSites.getSiteTable<CoreFilepoolPackageEntry, 'id'>(PACKAGES_TABLE_NAME, {
siteId, siteId,
config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy }, config: { cachingStrategy: CoreDatabaseCachingStrategy.Lazy },
onDestroy: () => delete this.packagesTables[siteId],
}), }),
), ),
); );
@ -149,16 +152,6 @@ export class CoreFilepoolProvider {
NgZone.run(() => this.checkQueueProcessing()); NgZone.run(() => this.checkQueueProcessing());
}); });
}); });
CoreEvents.on(CoreEvents.SITE_DELETED, async ({ siteId }) => {
if (!siteId || !(siteId in this.filesTables)) {
return;
}
await this.filesTables[siteId].destroy();
delete this.filesTables[siteId];
});
} }
/** /**

View File

@ -56,12 +56,8 @@ import { CoreAjaxError } from '@classes/errors/ajaxerror';
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror'; import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CorePromisedValue } from '@classes/promised-value'; import { CorePromisedValue } from '@classes/promised-value';
import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseConfiguration, CoreDatabaseTable } from '@classes/database/database-table';
import { import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy';
CoreDatabaseCachingStrategy,
CoreDatabaseConfiguration,
CoreDatabaseTableProxy,
} from '@classes/database/database-table-proxy';
import { asyncInstance, AsyncInstance } from '../utils/async-instance'; import { asyncInstance, AsyncInstance } from '../utils/async-instance';
import { CoreConfig } from './config'; import { CoreConfig } from './config';
@ -162,6 +158,7 @@ export class CoreSitesProvider {
config: Partial<CoreDatabaseConfiguration>; config: Partial<CoreDatabaseConfiguration>;
database: SQLiteDB; database: SQLiteDB;
primaryKeyColumns: PrimaryKeyColumn[]; primaryKeyColumns: PrimaryKeyColumn[];
onDestroy(): void;
}> = {}, }> = {},
): Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> { ): Promise<CoreDatabaseTable<DBRecord, PrimaryKeyColumn>> {
const siteId = options.siteId ?? this.getCurrentSiteId(); const siteId = options.siteId ?? this.getCurrentSiteId();
@ -180,6 +177,8 @@ export class CoreSitesProvider {
options.primaryKeyColumns, options.primaryKeyColumns,
); );
options.onDestroy && table.addListener({ onDestroy: options.onDestroy });
await table.initialize(); await table.initialize();
promisedTable.resolve(table as unknown as CoreDatabaseTable); promisedTable.resolve(table as unknown as CoreDatabaseTable);
@ -1837,16 +1836,19 @@ export class CoreSitesProvider {
* @returns Scehmas Table. * @returns Scehmas Table.
*/ */
protected getSiteSchemasTable(site: CoreSite): AsyncInstance<CoreDatabaseTable<SchemaVersionsDBEntry, 'name'>> { protected getSiteSchemasTable(site: CoreSite): AsyncInstance<CoreDatabaseTable<SchemaVersionsDBEntry, 'name'>> {
this.schemasTables[site.getId()] = this.schemasTables[site.getId()] ?? asyncInstance( const siteId = site.getId();
this.schemasTables[siteId] = this.schemasTables[siteId] ?? asyncInstance(
() => this.getSiteTable(SCHEMA_VERSIONS_TABLE_NAME, { () => this.getSiteTable(SCHEMA_VERSIONS_TABLE_NAME, {
siteId: site.getId(), siteId: siteId,
database: site.getDb(), database: site.getDb(),
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
primaryKeyColumns: ['name'], primaryKeyColumns: ['name'],
onDestroy: () => delete this.schemasTables[siteId],
}), }),
); );
return this.schemasTables[site.getId()]; return this.schemasTables[siteId];
} }
} }

View File

@ -32,7 +32,7 @@ export class CoreObject {
* @param b Second object. * @param b Second object.
* @return Whether objects are equal. * @return Whether objects are equal.
*/ */
static deepEquals(a: unknown, b: unknown): boolean { static deepEquals<T=unknown>(a: T, b: T): boolean {
return JSON.stringify(a) === JSON.stringify(b); return JSON.stringify(a) === JSON.stringify(b);
} }

View File

@ -17,7 +17,7 @@ import { CoreMainMenuLocalizedCustomItem } from '@features/mainmenu/services/mai
import { CoreSitesDemoSiteData } from '@services/sites'; import { CoreSitesDemoSiteData } from '@services/sites';
import { OpenFileAction } from '@services/utils/utils'; import { OpenFileAction } from '@services/utils/utils';
import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper'; import { CoreLoginSiteSelectorListMethod } from '@features/login/services/login-helper';
import { CoreDatabaseConfiguration } from '@classes/database/database-table-proxy'; import { CoreDatabaseConfiguration } from '@classes/database/database-table';
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */