• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vzakharchenko / forge-sql-orm / 14383671460

10 Apr 2025 02:56PM UTC coverage: 79.58% (+3.1%) from 76.514%
14383671460

Pull #41

github

web-flow
Merge 3630ae3c5 into 0fa3fd4a5
Pull Request #41: 2.0.19

219 of 274 branches covered (79.93%)

Branch coverage included in aggregate %.

178 of 191 new or added lines in 6 files covered. (93.19%)

3 existing lines in 2 files now uncovered.

919 of 1156 relevant lines covered (79.5%)

19.36 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.4
/src/core/ForgeSQLAnalizeOperations.ts
1
import {
2
  ForgeSqlOperation,
3
  SchemaAnalyzeForgeSql,
4
} from "./ForgeSQLQueryBuilder";
5
import { Query } from "drizzle-orm";
6
import {
7
  ClusterStatementRowCamelCase,
8
  ExplainAnalyzeRow,
9
  SlowQueryNormalized,
10
} from "./SystemTables";
11
import { SqlParameters } from "@forge/sql/out/sql-statement";
12
import { AnyMySqlTable } from "drizzle-orm/mysql-core/index";
13
import { getTableName } from "drizzle-orm/table";
2✔
14
import moment from "moment";
2✔
15

16
/**
17
 * Interface representing a row from the EXPLAIN ANALYZE output
18
 */
19
interface DecodedPlanRow {
20
  id: string;
21
  estRows?: string;
22
  estCost?: string;
23
  actRows?: string;
24
  task?: string;
25
  "access object"?: string;
26
  "execution info"?: string;
27
  "operator info"?: string;
28
  memory?: string;
29
  disk?: string;
30
}
31

32
/**
33
 * Interface representing a raw slow query row from the database
34
 */
35
interface SlowQueryRaw {
36
  Time: string;
37
  Txn_start_ts: number;
38
  User: string;
39
  Host: string;
40
  Conn_ID: number;
41
  DB: string;
42
  Query: string;
43
  Digest: string;
44
  Query_time: number;
45
  Compile_time: number;
46
  Optimize_time: number;
47
  Process_time: number;
48
  Wait_time: number;
49
  Parse_time: number;
50
  Rewrite_time: number;
51
  Cop_time: number;
52
  Cop_proc_avg: number;
53
  Cop_proc_max: number;
54
  Cop_proc_p90: number;
55
  Cop_proc_addr: string;
56
  Cop_wait_avg: number;
57
  Cop_wait_max: number;
58
  Cop_wait_p90: number;
59
  Cop_wait_addr: string;
60
  Mem_max: number;
61
  Disk_max: number;
62
  Total_keys: number;
63
  Process_keys: number;
64
  Request_count: number;
65
  KV_total: number;
66
  PD_total: number;
67
  Result_rows: number;
68
  Rocksdb_block_cache_hit_count: number;
69
  Rocksdb_block_read_count: number;
70
  Rocksdb_block_read_byte: number;
71
  Plan: string;
72
  Binary_plan: string;
73
  Plan_digest: string;
74
}
75

76
/**
77
 * Interface representing a row from the cluster statements table
78
 */
79
export interface ClusterStatementRow {
80
  INSTANCE: string;
81
  SUMMARY_BEGIN_TIME: string;
82
  SUMMARY_END_TIME: string;
83
  STMT_TYPE: string;
84
  SCHEMA_NAME: string;
85
  DIGEST: string;
86
  DIGEST_TEXT: string;
87
  TABLE_NAMES: string;
88
  INDEX_NAMES: string | null;
89
  SAMPLE_USER: string;
90
  EXEC_COUNT: number;
91
  SUM_ERRORS: number;
92
  SUM_WARNINGS: number;
93
  SUM_LATENCY: number;
94
  MAX_LATENCY: number;
95
  MIN_LATENCY: number;
96
  AVG_LATENCY: number;
97
  AVG_PARSE_LATENCY: number;
98
  MAX_PARSE_LATENCY: number;
99
  AVG_COMPILE_LATENCY: number;
100
  MAX_COMPILE_LATENCY: number;
101
  SUM_COP_TASK_NUM: number;
102
  MAX_COP_PROCESS_TIME: number;
103
  MAX_COP_PROCESS_ADDRESS: string;
104
  MAX_COP_WAIT_TIME: number;
105
  MAX_COP_WAIT_ADDRESS: string;
106
  AVG_PROCESS_TIME: number;
107
  MAX_PROCESS_TIME: number;
108
  AVG_WAIT_TIME: number;
109
  MAX_WAIT_TIME: number;
110
  AVG_BACKOFF_TIME: number;
111
  MAX_BACKOFF_TIME: number;
112
  AVG_TOTAL_KEYS: number;
113
  MAX_TOTAL_KEYS: number;
114
  AVG_PROCESSED_KEYS: number;
115
  MAX_PROCESSED_KEYS: number;
116
  AVG_ROCKSDB_DELETE_SKIPPED_COUNT: number;
117
  MAX_ROCKSDB_DELETE_SKIPPED_COUNT: number;
118
  AVG_ROCKSDB_KEY_SKIPPED_COUNT: number;
119
  MAX_ROCKSDB_KEY_SKIPPED_COUNT: number;
120
  AVG_ROCKSDB_BLOCK_CACHE_HIT_COUNT: number;
121
  MAX_ROCKSDB_BLOCK_CACHE_HIT_COUNT: number;
122
  AVG_ROCKSDB_BLOCK_READ_COUNT: number;
123
  MAX_ROCKSDB_BLOCK_READ_COUNT: number;
124
  AVG_ROCKSDB_BLOCK_READ_BYTE: number;
125
  MAX_ROCKSDB_BLOCK_READ_BYTE: number;
126
  AVG_PREWRITE_TIME: number;
127
  MAX_PREWRITE_TIME: number;
128
  AVG_COMMIT_TIME: number;
129
  MAX_COMMIT_TIME: number;
130
  AVG_GET_COMMIT_TS_TIME: number;
131
  MAX_GET_COMMIT_TS_TIME: number;
132
  AVG_COMMIT_BACKOFF_TIME: number;
133
  MAX_COMMIT_BACKOFF_TIME: number;
134
  AVG_RESOLVE_LOCK_TIME: number;
135
  MAX_RESOLVE_LOCK_TIME: number;
136
  AVG_LOCAL_LATCH_WAIT_TIME: number;
137
  MAX_LOCAL_LATCH_WAIT_TIME: number;
138
  AVG_WRITE_KEYS: number;
139
  MAX_WRITE_KEYS: number;
140
  AVG_WRITE_SIZE: number;
141
  MAX_WRITE_SIZE: number;
142
  AVG_PREWRITE_REGIONS: number;
143
  MAX_PREWRITE_REGIONS: number;
144
  AVG_TXN_RETRY: number;
145
  MAX_TXN_RETRY: number;
146
  SUM_EXEC_RETRY: number;
147
  SUM_EXEC_RETRY_TIME: number;
148
  SUM_BACKOFF_TIMES: number;
149
  BACKOFF_TYPES: string | null;
150
  AVG_MEM: number;
151
  MAX_MEM: number;
152
  AVG_DISK: number;
153
  MAX_DISK: number;
154
  AVG_KV_TIME: number;
155
  AVG_PD_TIME: number;
156
  AVG_BACKOFF_TOTAL_TIME: number;
157
  AVG_WRITE_SQL_RESP_TIME: number;
158
  AVG_TIDB_CPU_TIME: number;
159
  AVG_TIKV_CPU_TIME: number;
160
  MAX_RESULT_ROWS: number;
161
  MIN_RESULT_ROWS: number;
162
  AVG_RESULT_ROWS: number;
163
  PREPARED: number;
164
  AVG_AFFECTED_ROWS: number;
165
  FIRST_SEEN: string;
166
  LAST_SEEN: string;
167
  PLAN_IN_CACHE: number;
168
  PLAN_CACHE_HITS: number;
169
  PLAN_IN_BINDING: number;
170
  QUERY_SAMPLE_TEXT: string;
171
  PREV_SAMPLE_TEXT: string;
172
  PLAN_DIGEST: string;
173
  PLAN: string;
174
  BINARY_PLAN: string;
175
  CHARSET: string;
176
  COLLATION: string;
177
  PLAN_HINT: string;
178
  MAX_REQUEST_UNIT_READ: number;
179
  AVG_REQUEST_UNIT_READ: number;
180
  MAX_REQUEST_UNIT_WRITE: number;
181
  AVG_REQUEST_UNIT_WRITE: number;
182
  MAX_QUEUED_RC_TIME: number;
183
  AVG_QUEUED_RC_TIME: number;
184
  RESOURCE_GROUP: string;
185
  PLAN_CACHE_UNQUALIFIED: number;
186
  PLAN_CACHE_UNQUALIFIED_LAST_REASON: string;
187
}
188

189
/**
190
 * Class implementing SQL analysis operations for ForgeSQL ORM.
191
 * Provides methods for analyzing query performance, execution plans, and slow queries.
192
 */
193
export class ForgeSQLAnalizeOperation implements SchemaAnalyzeForgeSql {
2✔
194
  private readonly forgeOperations: ForgeSqlOperation;
32✔
195

196
  /**
197
   * Creates a new instance of ForgeSQLAnalizeOperation.
198
   * @param {ForgeSqlOperation} forgeOperations - The ForgeSQL operations instance
199
   */
200
  constructor(forgeOperations: ForgeSqlOperation) {
32✔
201
    this.forgeOperations = forgeOperations;
32✔
202
    this.mapToCamelCaseClusterStatement = this.mapToCamelCaseClusterStatement.bind(this);
32✔
203
  }
32✔
204

205
  /**
206
   * Executes EXPLAIN ANALYZE on a raw SQL query.
207
   * @param {string} query - The SQL query to analyze
208
   * @param {unknown[]} bindParams - The query parameters
209
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
210
   */
211
  async explainAnalyzeRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]> {
32✔
212
    const results = await this.forgeOperations
6✔
213
      .fetch()
6✔
214
      .executeRawSQL<DecodedPlanRow>(`EXPLAIN ANALYZE ${query}`, bindParams as SqlParameters);
6✔
215
    return results.map((row) => ({
6✔
216
      id: row.id,
4✔
217
      estRows: row.estRows,
4✔
218
      actRows: row.actRows,
4✔
219
      task: row.task,
4✔
220
      accessObject: row["access object"],
4✔
221
      executionInfo: row["execution info"],
4✔
222
      operatorInfo: row["operator info"],
4✔
223
      memory: row.memory,
4✔
224
      disk: row.disk,
4✔
225
    }));
6✔
226
  }
6✔
227

228
  /**
229
   * Executes EXPLAIN ANALYZE on a Drizzle query.
230
   * @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
231
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
232
   */
233
  async explainAnalyze(query: { toSQL: () => Query }): Promise<ExplainAnalyzeRow[]> {
32✔
234
    const { sql, params } = query.toSQL();
2✔
235
    return this.explainAnalyzeRaw(sql, params);
2✔
236
  }
2✔
237

238
  /**
239
   * Decodes a query execution plan from its string representation.
240
   * @param {string} input - The raw execution plan string
241
   * @returns {ExplainAnalyzeRow[]} The decoded execution plan rows
242
   */
243
  decodedPlan(input: string): ExplainAnalyzeRow[] {
32✔
244
    if (!input) {
18✔
245
      return [];
6✔
246
    }
6✔
247
    const lines = input.trim().split("\n");
12✔
248
    if (lines.length < 2) return [];
18✔
249

250
    const headersRaw = lines[0]
2✔
251
      .split("\t")
2✔
252
      .map((h) => h.trim())
2✔
253
      .filter(Boolean);
2✔
254
    const headers = headersRaw.map((h) => {
2✔
255
      return h
18✔
256
        .replace(/\s+/g, " ")
18✔
257
        .replace(/[-\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
18!
258
        .replace(/^./, (s) => s.toLowerCase());
18✔
259
    });
2✔
260

261
    return lines.slice(1).map((line) => {
2✔
262
      const values = line
2✔
263
        .split("\t")
2✔
264
        .map((s) => s.trim())
2✔
265
        .filter(Boolean);
2✔
266
      const row: Record<string, string> = {};
2✔
267
      headers.forEach((key, i) => {
2✔
268
        row[key] = values[i] ?? "";
18!
269
      });
2✔
270
      return row as unknown as ExplainAnalyzeRow;
2✔
271
    });
2✔
272
  }
18✔
273

274
  /**
275
   * Normalizes a raw slow query row into a more structured format.
276
   * @param {SlowQueryRaw} row - The raw slow query data
277
   * @returns {SlowQueryNormalized} The normalized slow query data
278
   */
279
  normalizeSlowQuery(row: SlowQueryRaw): SlowQueryNormalized {
32✔
280
    return {
4✔
281
      time: row.Time,
4✔
282
      txnStartTs: row.Txn_start_ts,
4✔
283
      user: row.User,
4✔
284
      host: row.Host,
4✔
285
      connId: row.Conn_ID,
4✔
286
      db: row.DB,
4✔
287
      query: row.Query,
4✔
288
      digest: row.Digest,
4✔
289
      queryTime: row.Query_time,
4✔
290
      compileTime: row.Compile_time,
4✔
291
      optimizeTime: row.Optimize_time,
4✔
292
      processTime: row.Process_time,
4✔
293
      waitTime: row.Wait_time,
4✔
294
      parseTime: row.Parse_time,
4✔
295
      rewriteTime: row.Rewrite_time,
4✔
296
      copTime: row.Cop_time,
4✔
297
      copProcAvg: row.Cop_proc_avg,
4✔
298
      copProcMax: row.Cop_proc_max,
4✔
299
      copProcP90: row.Cop_proc_p90,
4✔
300
      copProcAddr: row.Cop_proc_addr,
4✔
301
      copWaitAvg: row.Cop_wait_avg,
4✔
302
      copWaitMax: row.Cop_wait_max,
4✔
303
      copWaitP90: row.Cop_wait_p90,
4✔
304
      copWaitAddr: row.Cop_wait_addr,
4✔
305
      memMax: row.Mem_max,
4✔
306
      diskMax: row.Disk_max,
4✔
307
      totalKeys: row.Total_keys,
4✔
308
      processKeys: row.Process_keys,
4✔
309
      requestCount: row.Request_count,
4✔
310
      kvTotal: row.KV_total,
4✔
311
      pdTotal: row.PD_total,
4✔
312
      resultRows: row.Result_rows,
4✔
313
      rocksdbBlockCacheHitCount: row.Rocksdb_block_cache_hit_count,
4✔
314
      rocksdbBlockReadCount: row.Rocksdb_block_read_count,
4✔
315
      rocksdbBlockReadByte: row.Rocksdb_block_read_byte,
4✔
316
      plan: row.Plan,
4✔
317
      binaryPlan: row.Binary_plan,
4✔
318
      planDigest: row.Plan_digest,
4✔
319
      parsedPlan: this.decodedPlan(row.Plan),
4✔
320
    };
4✔
321
  }
4✔
322

323
  /**
324
   * Builds a SQL query for retrieving cluster statement history.
325
   * @param {string[]} tables - The tables to analyze
326
   * @param {Date} [from] - The start date for the analysis
327
   * @param {Date} [to] - The end date for the analysis
328
   * @returns {string} The SQL query for cluster statement history
329
   */
330
  buildClusterStatementQuery(tables: string[], from?: Date, to?: Date): string {
32✔
331
    const formatDateTime = (date: Date): string => moment(date).format("YYYY-MM-DDTHH:mm:ss.SSS");
8✔
332

333
    const tableConditions = tables
8✔
334
      .map((table) => `TABLE_NAMES = CONCAT(SCHEMA_NAME, '.', '${table}')`)
8✔
335
      .join(" OR ");
8✔
336

337
    const timeConditions: string[] = [];
8✔
338
    if (from) {
8✔
339
      timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime(from)}'`);
4✔
340
    }
4✔
341
    if (to) {
8✔
342
      timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime(to)}'`);
4✔
343
    }
4✔
344

345
    const whereClauses = [`(${tableConditions})`, ...timeConditions];
8✔
346

347
    return `
8✔
348
      SELECT *
349
      FROM (
350
        SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY
351
        UNION ALL
352
        SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
353
      ) AS combined
354
      WHERE ${whereClauses.join(" AND ")}
8✔
355
    `;
356
  }
8✔
357

358
  /**
359
   * Retrieves and analyzes slow queries from the database.
360
   * @returns {Promise<SlowQueryNormalized[]>} The normalized slow query data
361
   */
362
  async analyzeSlowQueries(): Promise<SlowQueryNormalized[]> {
32✔
363
    const results = await this.forgeOperations.fetch().executeRawSQL<SlowQueryRaw>(`
2✔
364
      SELECT *
365
      FROM information_schema.slow_query
366
      ORDER BY time DESC
367
    `);
2✔
368
    return results.map((row) => this.normalizeSlowQuery(row));
2✔
369
  }
2✔
370

371
  /**
372
   * Converts a cluster statement row to camelCase format.
373
   * @param {Record<string, any>} input - The input row data
374
   * @returns {ClusterStatementRowCamelCase} The converted row data
375
   */
376
  mapToCamelCaseClusterStatement(input: Record<string, any>): ClusterStatementRowCamelCase {
32✔
377
    if (!input) {
8!
NEW
378
      return {} as ClusterStatementRowCamelCase;
×
NEW
379
    }
×
380

381
    const result: any = {};
8✔
382
    result.parsedPlan = this.decodedPlan(input["PLAN"] || "");
8✔
383
    for (const key in input) {
8✔
384
      const camelKey = key.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
18✔
385
      result[camelKey] = input[key];
18✔
386
    }
18✔
387

388
    return result as ClusterStatementRowCamelCase;
8✔
389
  }
8✔
390

391
  /**
392
   * Analyzes query history for specific tables using raw table names.
393
   * @param {string[]} tables - The table names to analyze
394
   * @param {Date} [fromDate] - The start date for the analysis
395
   * @param {Date} [toDate] - The end date for the analysis
396
   * @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
397
   */
398
  async analyzeQueriesHistoryRaw(
32✔
399
    tables: string[],
4✔
400
    fromDate?: Date,
4✔
401
    toDate?: Date,
4✔
402
  ): Promise<ClusterStatementRowCamelCase[]> {
4✔
403
    const results = await this.forgeOperations
4✔
404
      .fetch()
4✔
405
      .executeRawSQL<ClusterStatementRow>(
4✔
406
        this.buildClusterStatementQuery(tables, fromDate, toDate),
4✔
407
      );
4✔
408
    return results.map((r) => this.mapToCamelCaseClusterStatement(r));
4✔
409
  }
4✔
410

411
  /**
412
   * Analyzes query history for specific tables using Drizzle table objects.
413
   * @param {AnyMySqlTable[]} tables - The Drizzle table objects to analyze
414
   * @param {Date} [fromDate] - The start date for the analysis
415
   * @param {Date} [toDate] - The end date for the analysis
416
   * @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
417
   */
418
  async analyzeQueriesHistory(
32✔
419
    tables: AnyMySqlTable[],
2✔
420
    fromDate?: Date,
2✔
421
    toDate?: Date,
2✔
422
  ): Promise<ClusterStatementRowCamelCase[]> {
2✔
423
    return this.analyzeQueriesHistoryRaw(
2✔
424
      tables.map((table) => getTableName(table)),
2✔
425
      fromDate,
2✔
426
      toDate,
2✔
427
    );
2✔
428
  }
2✔
429
}
32✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc