import chalk from "chalk"; import {distinct, Logger, pluralizeL, removeUndefined, splitWords} from "@azimutt/utils" import {Connector, getEntities, DatabaseQuery, DatabaseUrlParsed, EntityId, parseDatabaseUrl, QueryId} from "@azimutt/models" import {getConnector, track} from "@azimutt/gateway" import {version} from "./version.js"; import {fileWrite} from "./utils/file.js"; import {loggerNoOp} from "./utils/logger.js"; export type Opts = {} export async function launchClustering(url: string, opts: Opts, logger: Logger): Promise { const dbUrl: DatabaseUrlParsed = parseDatabaseUrl(url) const connector: Connector | undefined = getConnector(dbUrl) if (!connector) return Promise.reject(`Invalid connector for ${dbUrl.kind ? `${dbUrl.kind} db` : `unknown db (${dbUrl.full})`}`) const app = 'azimutt-cli-clustering' const queries: DatabaseQuery[] = await connector.getQueryHistory(app, dbUrl, {logger: loggerNoOp, database: dbUrl.db}).catch(err => { if (typeof err === 'string' && err === 'Not implemented') logger.log(chalk.blue(`Query history is not supported yet on ${dbUrl.kind}, ping us ;)`)) if (typeof err === 'object' && 'message' in err && err.message.indexOf('"pg_stat_statements" does not exist')) logger.log(chalk.blue(`Can't get query history as pg_stat_statements is not enabled. Enable it for a better db analysis.`)) return [] }) const selectQueries = queries.filter(q => q.query.trim().toLowerCase().startsWith('select ')) const selectJoinQueries = selectQueries.map(query => ({query, entities: getEntities(query.query)})).filter(q => q.entities.length > 1) track('cli__clustering__run', removeUndefined({version, database: dbUrl.kind, nb_queries: queries.length, nb_select_join: selectJoinQueries.length}), 'cli').then(() => {}) const links: Record> = {} selectJoinQueries.forEach(({query, entities}) => { const [main, ...joins] = distinct(entities.map(e => e.entity)) if (!links[main]) links[main] = {} joins.forEach(join => { if (!links[main][join]) { links[main][join] = [query.id] } else { links[main][join].push(query.id) } }) }) logger.log(`Found ${pluralizeL(queries, 'query')}, ${selectQueries.length} SELECTs and ${selectJoinQueries.length} JOINs.`) const path = 'clustering.html' await fileWrite(path, buildGraph(links)) logger.log(`Graph written in ${path}, open it to see the result`) } type GraphNode = {id: string, prefix: string} type GraphLink = {source: string, target: string, strength: number} function buildGraph(data: Record>): string { const nodes: GraphNode[] = [...new Set(Object.entries(data).flatMap(([k, v]) => [k, ...Object.keys(v)]))] .map(e => ({id: e, prefix: splitWords(e)[0]})) const links: GraphLink[] = Object.entries(data).flatMap(([source, targets]) => Object.entries(targets).map(([target, queries]) => ({source, target, strength: queries.length}) ) ) const maxStrength = Math.max(...links.map(l => l.strength)) // https://vasturiano.github.io/3d-force-graph return `
` }