2020-10-07 10:53:19 +02:00
|
|
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
import { Injectable } from '@angular/core';
|
|
|
|
|
|
|
|
import { SQLiteDB } from '@classes/sqlitedb';
|
2020-11-19 12:40:18 +01:00
|
|
|
import { SQLiteDBMock } from '@features/emulator/classes/sqlitedb';
|
2022-03-09 15:59:56 +01:00
|
|
|
import { CoreBrowser } from '@singletons/browser';
|
2022-06-20 18:19:18 +02:00
|
|
|
import { makeSingleton, SQLite } from '@singletons';
|
|
|
|
import { CorePlatform } from '@services/platform';
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2022-06-01 08:46:37 +02:00
|
|
|
const tableNameRegex = new RegExp([
|
|
|
|
'^SELECT.*FROM ([^ ]+)',
|
|
|
|
'^INSERT.*INTO ([^ ]+)',
|
|
|
|
'^UPDATE ([^ ]+)',
|
|
|
|
'^DELETE FROM ([^ ]+)',
|
|
|
|
'^CREATE TABLE IF NOT EXISTS ([^ ]+)',
|
|
|
|
'^ALTER TABLE ([^ ]+)',
|
|
|
|
'^DROP TABLE IF EXISTS ([^ ]+)',
|
|
|
|
].join('|'));
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
/**
|
|
|
|
* This service allows interacting with the local database to store and retrieve data.
|
|
|
|
*/
|
2020-11-19 16:35:17 +01:00
|
|
|
@Injectable({ providedIn: 'root' })
|
2020-10-07 10:53:19 +02:00
|
|
|
export class CoreDbProvider {
|
|
|
|
|
2022-01-24 18:00:27 +01:00
|
|
|
queryLogs: CoreDbQueryLog[] = [];
|
|
|
|
|
2020-10-14 08:29:58 +02:00
|
|
|
protected dbInstances: {[name: string]: SQLiteDB} = {};
|
2020-10-07 10:53:19 +02:00
|
|
|
|
2022-01-24 18:00:27 +01:00
|
|
|
/**
|
|
|
|
* Check whether database queries should be logged.
|
|
|
|
*
|
|
|
|
* @returns Whether queries should be logged.
|
|
|
|
*/
|
|
|
|
loggingEnabled(): boolean {
|
2023-01-17 18:08:02 +01:00
|
|
|
return CoreBrowser.hasDevelopmentSetting('DBLoggingEnabled');
|
2022-01-24 18:00:27 +01:00
|
|
|
}
|
|
|
|
|
2022-02-03 13:26:42 +01:00
|
|
|
/**
|
|
|
|
* Print query history in console.
|
2022-02-17 13:54:54 +01:00
|
|
|
*
|
2022-06-01 08:46:37 +02:00
|
|
|
* @param format Log format, with the following substitutions: :dbname, :sql, :duration, and :result.
|
2022-02-03 13:26:42 +01:00
|
|
|
*/
|
2022-06-01 08:46:37 +02:00
|
|
|
printHistory(format: string = ':dbname | :sql | Duration: :duration | Result: :result'): void {
|
|
|
|
const substituteParams = ({ sql, params, duration, error, dbName }: CoreDbQueryLog) => format
|
|
|
|
.replace(':dbname', dbName)
|
2022-02-17 13:54:54 +01:00
|
|
|
.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');
|
2022-02-03 13:26:42 +01:00
|
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.log(this.queryLogs.map(substituteParams).join('\n'));
|
|
|
|
}
|
|
|
|
|
2022-06-01 08:46:37 +02:00
|
|
|
/**
|
|
|
|
* Get the table name from a SQL query.
|
|
|
|
*
|
|
|
|
* @param sql SQL query.
|
2022-12-01 12:31:00 +01:00
|
|
|
* @returns Table name, undefined if not found.
|
2022-06-01 08:46:37 +02:00
|
|
|
*/
|
|
|
|
protected getTableNameFromSql(sql: string): string | undefined {
|
|
|
|
const matches = sql.match(tableNameRegex);
|
|
|
|
|
|
|
|
return matches?.find((matchEntry, index) => index > 0 && !!matchEntry);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a value matches a certain filter.
|
|
|
|
*
|
|
|
|
* @param value Value.
|
|
|
|
* @param filter Filter.
|
2022-12-01 12:31:00 +01:00
|
|
|
* @returns Whether the value matches the filter.
|
2022-06-01 08:46:37 +02:00
|
|
|
*/
|
|
|
|
protected valueMatchesFilter(value: string, filter?: RegExp | string): boolean {
|
|
|
|
if (typeof filter === 'string') {
|
|
|
|
return value === filter;
|
|
|
|
} else if (filter) {
|
|
|
|
return !!value.match(filter);
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build an object with the summary data for each db, table and statement.
|
|
|
|
*
|
|
|
|
* @param filters Filters to limit the data stored.
|
2022-12-01 12:31:00 +01:00
|
|
|
* @returns Object with the summary data.
|
2022-06-01 08:46:37 +02:00
|
|
|
*/
|
|
|
|
protected buildStatementsSummary(
|
|
|
|
filters: TablesSummaryFilters = {},
|
|
|
|
): Record<string, Record<string, Record<string, CoreDbStatementSummary>>> {
|
|
|
|
const statementsSummary: Record<string, Record<string, Record<string, CoreDbStatementSummary>>> = {};
|
|
|
|
|
|
|
|
this.queryLogs.forEach(log => {
|
|
|
|
if (!this.valueMatchesFilter(log.dbName, filters.dbName)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const statement = log.sql.substring(0, log.sql.indexOf(' '));
|
|
|
|
if (!statement) {
|
|
|
|
console.warn(`Statement not found from sql: ${log.sql}`); // eslint-disable-line no-console
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const tableName = this.getTableNameFromSql(log.sql);
|
|
|
|
if (!tableName) {
|
|
|
|
console.warn(`Table name not found from sql: ${log.sql}`); // eslint-disable-line no-console
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.valueMatchesFilter(tableName, filters.tableName)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
statementsSummary[log.dbName] = statementsSummary[log.dbName] ?? {};
|
|
|
|
statementsSummary[log.dbName][tableName] = statementsSummary[log.dbName][tableName] ?? {};
|
|
|
|
statementsSummary[log.dbName][tableName][statement] = statementsSummary[log.dbName][tableName][statement] ?? {
|
|
|
|
count: 0,
|
|
|
|
duration: 0,
|
|
|
|
errors: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
statementsSummary[log.dbName][tableName][statement].count++;
|
|
|
|
statementsSummary[log.dbName][tableName][statement].duration += log.duration;
|
|
|
|
if (log.error) {
|
|
|
|
statementsSummary[log.dbName][tableName][statement].errors++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return statementsSummary;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Print summary of statements for several tables.
|
|
|
|
*
|
|
|
|
* @param filters Filters to limit the results printed.
|
|
|
|
* @param format Log format, with the following substitutions: :dbname, :table, :statement, :count, :duration and :errors.
|
|
|
|
*/
|
|
|
|
printTablesSummary(
|
|
|
|
filters: TablesSummaryFilters = {},
|
|
|
|
format = ':dbname, :table, :statement, :count, :duration, :errors',
|
|
|
|
): void {
|
|
|
|
const statementsSummary = this.buildStatementsSummary(filters);
|
|
|
|
|
|
|
|
const substituteParams = (dbName: string, tableName: string, statementName: string) => format
|
|
|
|
.replace(':dbname', dbName)
|
|
|
|
.replace(':table', tableName)
|
|
|
|
.replace(':statement', statementName)
|
|
|
|
.replace(':count', String(statementsSummary[dbName][tableName][statementName].count))
|
2022-06-02 17:12:54 +02:00
|
|
|
.replace(':duration', statementsSummary[dbName][tableName][statementName].duration.toFixed(2) + 'ms')
|
2022-06-01 08:46:37 +02:00
|
|
|
.replace(':errors', String(statementsSummary[dbName][tableName][statementName].errors));
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
console.log(
|
|
|
|
Object.keys(statementsSummary)
|
|
|
|
.sort()
|
|
|
|
.map(dbName => Object.keys(statementsSummary[dbName])
|
|
|
|
.sort()
|
|
|
|
.map(tableName => Object.keys(statementsSummary[dbName][tableName])
|
|
|
|
.sort()
|
|
|
|
.map(statementName => substituteParams(dbName, tableName, statementName))
|
|
|
|
.join('\n')).join('\n')).join('\n'),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-01-24 18:00:27 +01:00
|
|
|
/**
|
|
|
|
* Log a query.
|
|
|
|
*
|
2022-02-17 13:54:54 +01:00
|
|
|
* @param log Query log.
|
2022-01-24 18:00:27 +01:00
|
|
|
*/
|
2022-02-17 13:54:54 +01:00
|
|
|
logQuery(log: CoreDbQueryLog): void {
|
|
|
|
this.queryLogs.push(log);
|
2022-01-24 18:00:27 +01:00
|
|
|
}
|
|
|
|
|
2022-06-01 08:46:37 +02:00
|
|
|
/**
|
|
|
|
* Clear stored logs.
|
|
|
|
*/
|
|
|
|
clearLogs(): void {
|
|
|
|
this.queryLogs = [];
|
|
|
|
}
|
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
/**
|
|
|
|
* Get or create a database object.
|
|
|
|
*
|
|
|
|
* The database objects are cached statically.
|
|
|
|
*
|
|
|
|
* @param name DB name.
|
|
|
|
* @param forceNew True if it should always create a new instance.
|
2022-12-01 12:31:00 +01:00
|
|
|
* @returns DB.
|
2020-10-07 10:53:19 +02:00
|
|
|
*/
|
|
|
|
getDB(name: string, forceNew?: boolean): SQLiteDB {
|
2021-12-16 10:46:40 +01:00
|
|
|
if (this.dbInstances[name] === undefined || forceNew) {
|
2022-06-20 18:19:18 +02:00
|
|
|
if (CorePlatform.is('cordova')) {
|
2020-10-07 10:53:19 +02:00
|
|
|
this.dbInstances[name] = new SQLiteDB(name);
|
|
|
|
} else {
|
|
|
|
this.dbInstances[name] = new SQLiteDBMock(name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.dbInstances[name];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a DB.
|
|
|
|
*
|
|
|
|
* @param name DB name.
|
2022-12-01 12:31:00 +01:00
|
|
|
* @returns Promise resolved when the DB is deleted.
|
2020-10-07 10:53:19 +02:00
|
|
|
*/
|
2020-10-14 08:29:58 +02:00
|
|
|
async deleteDB(name: string): Promise<void> {
|
2021-12-16 10:46:40 +01:00
|
|
|
if (this.dbInstances[name] !== undefined) {
|
2020-10-07 10:53:19 +02:00
|
|
|
// Close the database first.
|
2020-10-14 08:29:58 +02:00
|
|
|
await this.dbInstances[name].close();
|
2020-10-07 10:53:19 +02:00
|
|
|
|
|
|
|
const db = this.dbInstances[name];
|
|
|
|
delete this.dbInstances[name];
|
|
|
|
|
2020-10-14 08:29:58 +02:00
|
|
|
if (db instanceof SQLiteDBMock) {
|
|
|
|
// In WebSQL we cannot delete the database, just empty it.
|
|
|
|
return db.emptyDatabase();
|
|
|
|
} else {
|
2021-03-02 11:41:04 +01:00
|
|
|
return SQLite.deleteDatabase({
|
2020-10-07 10:53:19 +02:00
|
|
|
name,
|
2020-10-14 08:29:58 +02:00
|
|
|
location: 'default',
|
2020-10-07 10:53:19 +02:00
|
|
|
});
|
|
|
|
}
|
2022-06-20 18:19:18 +02:00
|
|
|
} else if (CorePlatform.is('cordova')) {
|
2021-03-02 11:41:04 +01:00
|
|
|
return SQLite.deleteDatabase({
|
2020-10-14 08:29:58 +02:00
|
|
|
name,
|
|
|
|
location: 'default',
|
|
|
|
});
|
|
|
|
}
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
2020-10-14 08:29:58 +02:00
|
|
|
|
2020-10-07 10:53:19 +02:00
|
|
|
}
|
|
|
|
|
2021-03-02 11:41:04 +01:00
|
|
|
export const CoreDB = makeSingleton(CoreDbProvider);
|
2022-01-24 18:00:27 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Database query log entry.
|
|
|
|
*/
|
|
|
|
export interface CoreDbQueryLog {
|
2022-06-01 08:46:37 +02:00
|
|
|
dbName: string;
|
2022-01-24 18:00:27 +01:00
|
|
|
sql: string;
|
|
|
|
duration: number;
|
2022-02-17 13:54:54 +01:00
|
|
|
error?: Error;
|
2022-01-24 18:00:27 +01:00
|
|
|
params?: unknown[];
|
|
|
|
}
|
2022-06-01 08:46:37 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Summary about a certain DB statement.
|
|
|
|
*/
|
|
|
|
type CoreDbStatementSummary = {
|
|
|
|
count: number;
|
|
|
|
duration: number;
|
|
|
|
errors: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Filters to print tables summary.
|
|
|
|
*/
|
|
|
|
type TablesSummaryFilters = {
|
|
|
|
dbName?: RegExp | string;
|
|
|
|
tableName?: RegExp | string;
|
|
|
|
};
|