Merge pull request #3305 from dpalou/MOBILE-4087

MOBILE-4087 debug: Allow printing tables db summary
main
Noel De Martin 2022-06-01 13:17:46 +02:00 committed by GitHub
commit 49bd11b39c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 163 additions and 3 deletions

View File

@ -1169,6 +1169,8 @@ export class SQLiteDB {
* @returns Spy methods.
*/
protected getDatabaseSpies(db: SQLiteObject): Partial<SQLiteObject> {
const dbName = this.name;
return {
async executeSql(statement, params) {
const start = performance.now();
@ -1180,6 +1182,7 @@ export class SQLiteDB {
params,
sql: statement,
duration: performance.now() - start,
dbName,
});
return result;
@ -1189,6 +1192,7 @@ export class SQLiteDB {
error,
sql: statement,
duration: performance.now() - start,
dbName,
});
throw error;
@ -1206,6 +1210,7 @@ export class SQLiteDB {
CoreDB.logQuery({
sql,
duration: performance.now() - start,
dbName,
});
return result;
@ -1214,6 +1219,7 @@ export class SQLiteDB {
sql,
error,
duration: performance.now() - start,
dbName,
});
throw error;

View File

@ -171,6 +171,8 @@ export class SQLiteDBMock extends SQLiteDB {
* @inheritdoc
*/
protected getDatabaseSpies(db: SQLiteObject): Partial<SQLiteObject> {
const dbName = this.name;
return {
transaction: (callback) => db.transaction((transaction) => {
const transactionSpy: DbTransaction = {
@ -185,6 +187,7 @@ export class SQLiteDBMock extends SQLiteDB {
sql,
params,
duration: performance.now() - start,
dbName,
});
return success?.(...args);
@ -195,6 +198,7 @@ export class SQLiteDBMock extends SQLiteDB {
params,
error: args[0],
duration: performance.now() - start,
dbName,
});
return error?.(...args);

View File

@ -20,6 +20,16 @@ import { CoreBrowser } from '@singletons/browser';
import { makeSingleton, SQLite, Platform } from '@singletons';
import { CoreAppProvider } from './app';
const tableNameRegex = new RegExp([
'^SELECT.*FROM ([^ ]+)',
'^INSERT.*INTO ([^ ]+)',
'^UPDATE ([^ ]+)',
'^DELETE FROM ([^ ]+)',
'^CREATE TABLE IF NOT EXISTS ([^ ]+)',
'^ALTER TABLE ([^ ]+)',
'^DROP TABLE IF EXISTS ([^ ]+)',
].join('|'));
/**
* This service allows interacting with the local database to store and retrieve data.
*/
@ -42,10 +52,11 @@ export class CoreDbProvider {
/**
* Print query history in console.
*
* @param format Log format, with the following substitutions: :sql, :duration, and :result.
* @param format Log format, with the following substitutions: :dbname, :sql, :duration, and :result.
*/
printHistory(format: string = ':sql | Duration: :duration | Result: :result'): void {
const substituteParams = ({ sql, params, duration, error }: CoreDbQueryLog) => format
printHistory(format: string = ':dbname | :sql | Duration: :duration | Result: :result'): void {
const substituteParams = ({ sql, params, duration, error, dbName }: CoreDbQueryLog) => format
.replace(':dbname', dbName)
.replace(':sql', Object
.values(params ?? [])
.reduce((sql: string, param: string) => sql.replace('?', param) as string, sql) as string)
@ -56,6 +67,120 @@ export class CoreDbProvider {
console.log(this.queryLogs.map(substituteParams).join('\n'));
}
/**
* Get the table name from a SQL query.
*
* @param sql SQL query.
* @return Table name, undefined if not found.
*/
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.
* @return Whether the value matches the filter.
*/
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.
* @return Object with the summary data.
*/
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))
.replace(':duration', statementsSummary[dbName][tableName][statementName].duration + 'ms')
.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'),
);
}
/**
* Log a query.
*
@ -65,6 +190,13 @@ export class CoreDbProvider {
this.queryLogs.push(log);
}
/**
* Clear stored logs.
*/
clearLogs(): void {
this.queryLogs = [];
}
/**
* Get or create a database object.
*
@ -125,8 +257,26 @@ export const CoreDB = makeSingleton(CoreDbProvider);
* Database query log entry.
*/
export interface CoreDbQueryLog {
dbName: string;
sql: string;
duration: number;
error?: Error;
params?: unknown[];
}
/**
* 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;
};