forge-sql-orm 2.1.11 → 2.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +800 -541
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.js +257 -0
- package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -0
- package/dist/core/ForgeSQLCacheOperations.js +172 -0
- package/dist/core/ForgeSQLCacheOperations.js.map +1 -0
- package/dist/core/ForgeSQLCrudOperations.js +349 -0
- package/dist/core/ForgeSQLCrudOperations.js.map +1 -0
- package/dist/core/ForgeSQLORM.d.ts +1 -1
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.js +1191 -0
- package/dist/core/ForgeSQLORM.js.map +1 -0
- package/dist/core/ForgeSQLQueryBuilder.js +77 -0
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -0
- package/dist/core/ForgeSQLSelectOperations.js +81 -0
- package/dist/core/ForgeSQLSelectOperations.js.map +1 -0
- package/dist/core/SystemTables.js +258 -0
- package/dist/core/SystemTables.js.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +527 -0
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -0
- package/dist/utils/cacheContextUtils.d.ts.map +1 -1
- package/dist/utils/cacheContextUtils.js +198 -0
- package/dist/utils/cacheContextUtils.js.map +1 -0
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +383 -0
- package/dist/utils/cacheUtils.js.map +1 -0
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +139 -0
- package/dist/utils/forgeDriver.js.map +1 -0
- package/dist/utils/forgeDriverProxy.js +68 -0
- package/dist/utils/forgeDriverProxy.js.map +1 -0
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.js +28 -0
- package/dist/utils/metadataContextUtils.js.map +1 -0
- package/dist/utils/requestTypeContextUtils.js +10 -0
- package/dist/utils/requestTypeContextUtils.js.map +1 -0
- package/dist/utils/sqlHints.js +52 -0
- package/dist/utils/sqlHints.js.map +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +590 -0
- package/dist/utils/sqlUtils.js.map +1 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.js +77 -0
- package/dist/webtriggers/applyMigrationsWebTrigger.js.map +1 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.js +83 -0
- package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -0
- package/dist/webtriggers/dropMigrationWebTrigger.js +54 -0
- package/dist/webtriggers/dropMigrationWebTrigger.js.map +1 -0
- package/dist/webtriggers/dropTablesMigrationWebTrigger.js +54 -0
- package/dist/webtriggers/dropTablesMigrationWebTrigger.js.map +1 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.js +82 -0
- package/dist/webtriggers/fetchSchemaWebTrigger.js.map +1 -0
- package/dist/webtriggers/index.js +40 -0
- package/dist/webtriggers/index.js.map +1 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -1
- package/dist/webtriggers/slowQuerySchedulerTrigger.js +80 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.js.map +1 -0
- package/package.json +28 -23
- package/src/core/ForgeSQLAnalyseOperations.ts +3 -2
- package/src/core/ForgeSQLORM.ts +33 -27
- package/src/lib/drizzle/extensions/additionalActions.ts +11 -0
- package/src/utils/cacheContextUtils.ts +9 -6
- package/src/utils/cacheUtils.ts +28 -5
- package/src/utils/forgeDriver.ts +10 -6
- package/src/utils/metadataContextUtils.ts +1 -4
- package/src/utils/sqlUtils.ts +136 -125
- package/src/webtriggers/slowQuerySchedulerTrigger.ts +40 -33
- package/dist/ForgeSQLORM.js +0 -3896
- package/dist/ForgeSQLORM.js.map +0 -1
- package/dist/ForgeSQLORM.mjs +0 -3879
- package/dist/ForgeSQLORM.mjs.map +0 -1
package/src/utils/sqlUtils.ts
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
AnyColumn,
|
|
4
4
|
Column,
|
|
5
5
|
gte,
|
|
6
|
-
|
|
6
|
+
ilike,
|
|
7
7
|
isNotNull,
|
|
8
8
|
isTable,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
ne,
|
|
10
|
+
not,
|
|
11
11
|
notInArray,
|
|
12
12
|
SQL,
|
|
13
13
|
sql,
|
|
@@ -20,16 +20,14 @@ import { AnyIndexBuilder } from "drizzle-orm/mysql-core/indexes";
|
|
|
20
20
|
import { CheckBuilder } from "drizzle-orm/mysql-core/checks";
|
|
21
21
|
import { ForeignKeyBuilder } from "drizzle-orm/mysql-core/foreign-keys";
|
|
22
22
|
import { UniqueConstraintBuilder } from "drizzle-orm/mysql-core/unique-constraint";
|
|
23
|
-
import {
|
|
24
|
-
SelectedFields
|
|
25
|
-
} from "drizzle-orm/mysql-core/query-builders/select.types";
|
|
23
|
+
import { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
|
|
26
24
|
import { MySqlTable } from "drizzle-orm/mysql-core";
|
|
27
25
|
import { isSQLWrapper } from "drizzle-orm/sql/sql";
|
|
28
|
-
import {clusterStatementsSummary, slowQuery} from "../core/SystemTables";
|
|
26
|
+
import { clusterStatementsSummary, slowQuery } from "../core/SystemTables";
|
|
29
27
|
import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
|
|
30
|
-
import { ColumnDataType} from "drizzle-orm/column-builder";
|
|
31
|
-
import {AnyMySqlColumn} from "drizzle-orm/mysql-core/columns/common";
|
|
32
|
-
import type {ColumnBaseConfig} from "drizzle-orm/column";
|
|
28
|
+
import { ColumnDataType } from "drizzle-orm/column-builder";
|
|
29
|
+
import { AnyMySqlColumn } from "drizzle-orm/mysql-core/columns/common";
|
|
30
|
+
import type { ColumnBaseConfig } from "drizzle-orm/column";
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Interface representing table metadata information
|
|
@@ -95,7 +93,7 @@ export const parseDateTime = (value: string | Date, format: string): Date => {
|
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
// 4. Ensure the result is a valid Date object
|
|
98
|
-
if (isNaN(result.getTime())) {
|
|
96
|
+
if (Number.isNaN(result.getTime())) {
|
|
99
97
|
result = new Date(value);
|
|
100
98
|
}
|
|
101
99
|
return result;
|
|
@@ -129,7 +127,7 @@ export function formatDateTime(
|
|
|
129
127
|
}
|
|
130
128
|
if (!dt?.isValid) {
|
|
131
129
|
const parsed = Number(value);
|
|
132
|
-
if (!isNaN(parsed)) {
|
|
130
|
+
if (!Number.isNaN(parsed)) {
|
|
133
131
|
dt = DateTime.fromMillis(parsed);
|
|
134
132
|
}
|
|
135
133
|
}
|
|
@@ -185,17 +183,15 @@ export function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyC
|
|
|
185
183
|
// Collect all primary key columns from all primary key builders
|
|
186
184
|
const primaryKeyColumns = new Set<[string, AnyColumn]>();
|
|
187
185
|
|
|
188
|
-
|
|
186
|
+
for (const primaryKeyBuilder of primaryKeys) {
|
|
189
187
|
// Get primary key columns from each builder
|
|
190
|
-
Object.entries(columns)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
});
|
|
198
|
-
});
|
|
188
|
+
for (const [name, column1] of Object.entries(columns).filter(([, column]) => {
|
|
189
|
+
// @ts-ignore - PrimaryKeyBuilder has internal columns property
|
|
190
|
+
return primaryKeyBuilder.columns.includes(column);
|
|
191
|
+
})) {
|
|
192
|
+
primaryKeyColumns.add([name, column1]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
199
195
|
|
|
200
196
|
return Array.from(primaryKeyColumns);
|
|
201
197
|
}
|
|
@@ -222,12 +218,12 @@ function processForeignKeys(
|
|
|
222
218
|
// @ts-ignore
|
|
223
219
|
const fkArray: any[] = table[foreignKeysSymbol];
|
|
224
220
|
if (fkArray) {
|
|
225
|
-
|
|
221
|
+
for (const fk of fkArray) {
|
|
226
222
|
if (fk.reference) {
|
|
227
223
|
const item = fk.reference(fk);
|
|
228
224
|
foreignKeys.push(item);
|
|
229
225
|
}
|
|
230
|
-
}
|
|
226
|
+
}
|
|
231
227
|
}
|
|
232
228
|
}
|
|
233
229
|
|
|
@@ -244,14 +240,14 @@ function processForeignKeys(
|
|
|
244
240
|
(item) => (item as ConfigBuilderData).value ?? item,
|
|
245
241
|
);
|
|
246
242
|
|
|
247
|
-
|
|
248
|
-
if (!builder?.constructor)
|
|
243
|
+
for (const builder of configBuilders) {
|
|
244
|
+
if (!builder?.constructor) continue;
|
|
249
245
|
|
|
250
246
|
const builderName = builder.constructor.name.toLowerCase();
|
|
251
247
|
if (builderName.includes("foreignkeybuilder")) {
|
|
252
248
|
foreignKeys.push(builder);
|
|
253
249
|
}
|
|
254
|
-
}
|
|
250
|
+
}
|
|
255
251
|
}
|
|
256
252
|
}
|
|
257
253
|
}
|
|
@@ -299,8 +295,8 @@ export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
|
|
|
299
295
|
);
|
|
300
296
|
|
|
301
297
|
// Process each builder
|
|
302
|
-
|
|
303
|
-
if (!builder?.constructor)
|
|
298
|
+
for (const builder of configBuilders) {
|
|
299
|
+
if (!builder?.constructor) continue;
|
|
304
300
|
|
|
305
301
|
const builderName = builder.constructor.name.toLowerCase();
|
|
306
302
|
|
|
@@ -322,7 +318,7 @@ export function getTableMetadata(table: AnyMySqlTable): MetadataInfo {
|
|
|
322
318
|
|
|
323
319
|
// Always add to extras array
|
|
324
320
|
builders.extras.push(builder);
|
|
325
|
-
}
|
|
321
|
+
}
|
|
326
322
|
}
|
|
327
323
|
}
|
|
328
324
|
}
|
|
@@ -354,14 +350,14 @@ export function generateDropTableStatements(
|
|
|
354
350
|
console.warn('No drop operations requested: both "table" and "sequence" options are false');
|
|
355
351
|
return [];
|
|
356
352
|
}
|
|
357
|
-
|
|
353
|
+
for (const tableName of tables) {
|
|
358
354
|
if (validOptions.table) {
|
|
359
355
|
dropStatements.push(`DROP TABLE IF EXISTS \`${tableName}\`;`);
|
|
360
356
|
}
|
|
361
357
|
if (validOptions.sequence) {
|
|
362
358
|
dropStatements.push(`DROP SEQUENCE IF EXISTS \`${tableName}\`;`);
|
|
363
359
|
}
|
|
364
|
-
}
|
|
360
|
+
}
|
|
365
361
|
|
|
366
362
|
return dropStatements;
|
|
367
363
|
}
|
|
@@ -375,13 +371,13 @@ function mapSelectTableToAlias(
|
|
|
375
371
|
): any {
|
|
376
372
|
const { columns, tableName } = getTableMetadata(table);
|
|
377
373
|
const selectionsTableFields: Record<string, unknown> = {};
|
|
378
|
-
Object.keys(columns)
|
|
374
|
+
for (const name of Object.keys(columns)) {
|
|
379
375
|
const column = columns[name] as AnyColumn;
|
|
380
376
|
const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();
|
|
381
377
|
const fieldAlias = sql.raw(uniqName);
|
|
382
378
|
selectionsTableFields[name] = sql`${column} as \`${fieldAlias}\``;
|
|
383
379
|
aliasMap[uniqName] = column;
|
|
384
|
-
}
|
|
380
|
+
}
|
|
385
381
|
return selectionsTableFields;
|
|
386
382
|
}
|
|
387
383
|
|
|
@@ -408,9 +404,9 @@ export function mapSelectAllFieldsToAlias(
|
|
|
408
404
|
selections[name] = fields;
|
|
409
405
|
} else {
|
|
410
406
|
const innerSelections: any = {};
|
|
411
|
-
|
|
407
|
+
for (const [iname, ifields] of Object.entries(fields)) {
|
|
412
408
|
mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);
|
|
413
|
-
}
|
|
409
|
+
}
|
|
414
410
|
selections[name] = innerSelections;
|
|
415
411
|
}
|
|
416
412
|
return selections;
|
|
@@ -423,9 +419,10 @@ export function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(
|
|
|
423
419
|
}
|
|
424
420
|
const aliasMap: AliasColumnMap = {};
|
|
425
421
|
const selections: any = {};
|
|
426
|
-
Object.entries(fields).
|
|
427
|
-
|
|
428
|
-
|
|
422
|
+
for (let i = 0; i < Object.entries(fields).length; i++) {
|
|
423
|
+
const [name, fields1] = Object.entries(fields)[i];
|
|
424
|
+
mapSelectAllFieldsToAlias(selections, name, name, fields1, aliasMap);
|
|
425
|
+
}
|
|
429
426
|
return { selections, aliasMap };
|
|
430
427
|
}
|
|
431
428
|
|
|
@@ -548,7 +545,7 @@ function processNullBranches(obj: Record<string, unknown>): Record<string, unkno
|
|
|
548
545
|
}
|
|
549
546
|
|
|
550
547
|
export function formatLimitOffset(limitOrOffset: number): number {
|
|
551
|
-
if (typeof limitOrOffset !== "number" || isNaN(limitOrOffset)) {
|
|
548
|
+
if (typeof limitOrOffset !== "number" || Number.isNaN(limitOrOffset)) {
|
|
552
549
|
throw new Error("limitOrOffset must be a valid number");
|
|
553
550
|
}
|
|
554
551
|
return sql.raw(`${limitOrOffset}`) as unknown as number;
|
|
@@ -591,8 +588,8 @@ export async function printQueriesWithPlan(
|
|
|
591
588
|
) {
|
|
592
589
|
try {
|
|
593
590
|
const statementsTable = clusterStatementsSummary;
|
|
594
|
-
|
|
595
|
-
|
|
591
|
+
const timeoutMs = timeout ?? 3000;
|
|
592
|
+
const results = await withTimeout(
|
|
596
593
|
forgeSQLORM
|
|
597
594
|
.getDrizzleQueryBuilder()
|
|
598
595
|
.select({
|
|
@@ -601,14 +598,21 @@ export async function printQueriesWithPlan(
|
|
|
601
598
|
avgMem: statementsTable.avgMem,
|
|
602
599
|
execCount: statementsTable.execCount,
|
|
603
600
|
plan: statementsTable.plan,
|
|
604
|
-
|
|
601
|
+
stmtType: statementsTable.stmtType,
|
|
605
602
|
})
|
|
606
603
|
.from(statementsTable)
|
|
607
604
|
.where(
|
|
608
605
|
and(
|
|
609
606
|
isNotNull(statementsTable.digest),
|
|
610
|
-
|
|
611
|
-
notInArray(statementsTable.stmtType, [
|
|
607
|
+
not(ilike(statementsTable.digestText, "%information_schema%")),
|
|
608
|
+
notInArray(statementsTable.stmtType, [
|
|
609
|
+
"Use",
|
|
610
|
+
"Set",
|
|
611
|
+
"Show",
|
|
612
|
+
"Commit",
|
|
613
|
+
"Rollback",
|
|
614
|
+
"Begin",
|
|
615
|
+
]),
|
|
612
616
|
gte(
|
|
613
617
|
statementsTable.lastSeen,
|
|
614
618
|
sql`DATE_SUB
|
|
@@ -619,12 +623,11 @@ export async function printQueriesWithPlan(
|
|
|
619
623
|
),
|
|
620
624
|
),
|
|
621
625
|
),
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
timeoutMs+200,
|
|
626
|
+
`Timeout ${timeoutMs}ms in printQueriesWithPlan - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
|
|
627
|
+
timeoutMs + 200,
|
|
625
628
|
);
|
|
626
629
|
|
|
627
|
-
|
|
630
|
+
for (const result of results) {
|
|
628
631
|
// Average execution time (convert from nanoseconds to milliseconds)
|
|
629
632
|
const avgTimeMs = Number(result.avgLatency) / 1_000_000;
|
|
630
633
|
const avgMemMB = Number(result.avgMem) / 1_000_000;
|
|
@@ -634,7 +637,7 @@ export async function printQueriesWithPlan(
|
|
|
634
637
|
console.warn(
|
|
635
638
|
`SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${result.execCount}\n Plan:${result.plan}`,
|
|
636
639
|
);
|
|
637
|
-
}
|
|
640
|
+
}
|
|
638
641
|
} catch (error) {
|
|
639
642
|
// eslint-disable-next-line no-console
|
|
640
643
|
console.debug(
|
|
@@ -644,11 +647,11 @@ export async function printQueriesWithPlan(
|
|
|
644
647
|
}
|
|
645
648
|
}
|
|
646
649
|
|
|
647
|
-
const SESSION_ALIAS_NAME_ORM =
|
|
650
|
+
const SESSION_ALIAS_NAME_ORM = "orm";
|
|
648
651
|
|
|
649
652
|
/**
|
|
650
653
|
* Analyzes and logs slow queries from the last specified number of hours.
|
|
651
|
-
*
|
|
654
|
+
*
|
|
652
655
|
* This function queries the slow query system table to find queries that were executed
|
|
653
656
|
* within the specified time window and logs detailed performance information including:
|
|
654
657
|
* - SQL query text
|
|
@@ -656,77 +659,81 @@ const SESSION_ALIAS_NAME_ORM = 'orm';
|
|
|
656
659
|
* - Query execution time (in ms)
|
|
657
660
|
* - Execution count
|
|
658
661
|
* - Execution plan
|
|
659
|
-
*
|
|
662
|
+
*
|
|
660
663
|
* @param forgeSQLORM - The ForgeSQL operation instance for database access
|
|
661
664
|
* @param hours - Number of hours to look back for slow queries (e.g., 1 for last hour, 24 for last day)
|
|
662
665
|
* @param timeout - Optional timeout in milliseconds for the query execution (defaults to 1500ms)
|
|
663
|
-
*
|
|
666
|
+
*
|
|
664
667
|
* @example
|
|
665
668
|
* ```typescript
|
|
666
669
|
* // Analyze slow queries from the last hour
|
|
667
670
|
* await slowQueryPerHours(forgeSQLORM, 1);
|
|
668
|
-
*
|
|
671
|
+
*
|
|
669
672
|
* // Analyze slow queries from the last 24 hours with custom timeout
|
|
670
673
|
* await slowQueryPerHours(forgeSQLORM, 24, 3000);
|
|
671
|
-
*
|
|
674
|
+
*
|
|
672
675
|
* // Analyze slow queries from the last 6 hours
|
|
673
676
|
* await slowQueryPerHours(forgeSQLORM, 6);
|
|
674
677
|
* ```
|
|
675
|
-
*
|
|
678
|
+
*
|
|
676
679
|
* @throws Does not throw - errors are logged to console.debug instead
|
|
677
680
|
*/
|
|
678
|
-
export async function slowQueryPerHours(
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
681
|
+
export async function slowQueryPerHours(
|
|
682
|
+
forgeSQLORM: ForgeSqlOperation,
|
|
683
|
+
hours: number,
|
|
684
|
+
timeout?: number,
|
|
685
|
+
) {
|
|
686
|
+
try {
|
|
687
|
+
const timeoutMs = timeout ?? 1500;
|
|
688
|
+
const results = await withTimeout(
|
|
689
|
+
forgeSQLORM
|
|
690
|
+
.getDrizzleQueryBuilder()
|
|
691
|
+
.select({
|
|
692
|
+
query: withTidbHint(slowQuery.query),
|
|
693
|
+
queryTime: slowQuery.queryTime,
|
|
694
|
+
memMax: slowQuery.memMax,
|
|
695
|
+
plan: slowQuery.plan,
|
|
696
|
+
})
|
|
697
|
+
.from(slowQuery)
|
|
698
|
+
.where(
|
|
699
|
+
and(
|
|
700
|
+
isNotNull(slowQuery.digest),
|
|
701
|
+
ne(slowQuery.sessionAlias, SESSION_ALIAS_NAME_ORM),
|
|
702
|
+
gte(
|
|
703
|
+
slowQuery.time,
|
|
704
|
+
sql`DATE_SUB
|
|
698
705
|
(NOW(), INTERVAL
|
|
699
706
|
${hours}
|
|
700
707
|
HOUR
|
|
701
708
|
)`,
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
message,
|
|
719
|
-
);
|
|
720
|
-
});
|
|
721
|
-
return response;
|
|
722
|
-
} catch (error) {
|
|
723
|
-
// eslint-disable-next-line no-console
|
|
724
|
-
console.debug(
|
|
725
|
-
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
|
|
726
|
-
error,
|
|
727
|
-
);
|
|
728
|
-
return [`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`]
|
|
709
|
+
),
|
|
710
|
+
),
|
|
711
|
+
),
|
|
712
|
+
`Timeout ${timeoutMs}ms in slowQueryPerHours - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
|
|
713
|
+
timeoutMs,
|
|
714
|
+
);
|
|
715
|
+
const response: string[] = [];
|
|
716
|
+
for (const result of results) {
|
|
717
|
+
// Convert memory from bytes to MB and handle null values
|
|
718
|
+
const memMaxMB = result.memMax ? Number(result.memMax) / 1_000_000 : 0;
|
|
719
|
+
|
|
720
|
+
const message = `Found SlowQuery SQL: ${result.query} | Memory: ${memMaxMB.toFixed(2)} MB | Time: ${result.queryTime} ms\n Plan:${result.plan}`;
|
|
721
|
+
response.push(message);
|
|
722
|
+
// 1. Query info: SQL, memory, time, executions
|
|
723
|
+
// eslint-disable-next-line no-console
|
|
724
|
+
console.warn(message);
|
|
729
725
|
}
|
|
726
|
+
return response;
|
|
727
|
+
} catch (error) {
|
|
728
|
+
// eslint-disable-next-line no-console
|
|
729
|
+
console.debug(
|
|
730
|
+
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
|
|
731
|
+
error,
|
|
732
|
+
);
|
|
733
|
+
return [
|
|
734
|
+
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
735
|
+
];
|
|
736
|
+
}
|
|
730
737
|
}
|
|
731
738
|
|
|
732
739
|
/**
|
|
@@ -737,29 +744,33 @@ export async function slowQueryPerHours(forgeSQLORM: ForgeSqlOperation, hours:nu
|
|
|
737
744
|
* @returns Promise that resolves with the result or rejects on timeout
|
|
738
745
|
* @throws {Error} When the operation times out
|
|
739
746
|
*/
|
|
740
|
-
export async function withTimeout<T>(
|
|
741
|
-
|
|
747
|
+
export async function withTimeout<T>(
|
|
748
|
+
promise: Promise<T>,
|
|
749
|
+
message: string,
|
|
750
|
+
timeoutMs: number,
|
|
751
|
+
): Promise<T> {
|
|
752
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
753
|
+
|
|
754
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
755
|
+
timeoutId = setTimeout(() => {
|
|
756
|
+
reject(new Error(message));
|
|
757
|
+
}, timeoutMs);
|
|
758
|
+
});
|
|
742
759
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}, timeoutMs);
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
try {
|
|
752
|
-
return await Promise.race([promise, timeoutPromise]);
|
|
753
|
-
} finally {
|
|
754
|
-
if (timeoutId) {
|
|
755
|
-
clearTimeout(timeoutId);
|
|
756
|
-
}
|
|
760
|
+
try {
|
|
761
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
762
|
+
} finally {
|
|
763
|
+
if (timeoutId) {
|
|
764
|
+
clearTimeout(timeoutId);
|
|
757
765
|
}
|
|
766
|
+
}
|
|
758
767
|
}
|
|
759
768
|
|
|
760
|
-
export function withTidbHint<
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
769
|
+
export function withTidbHint<
|
|
770
|
+
TDataType extends ColumnDataType,
|
|
771
|
+
TPartial extends Partial<ColumnBaseConfig<TDataType, string>>,
|
|
772
|
+
>(column: AnyMySqlColumn<TPartial>): AnyMySqlColumn<TPartial> {
|
|
773
|
+
// We lie a bit to TypeScript here: at runtime this is a new SQL fragment,
|
|
774
|
+
// but returning TExpr keeps the column type info in downstream inference.
|
|
775
|
+
return sql`/*+ SET_VAR(tidb_session_alias=${sql.raw(`${SESSION_ALIAS_NAME_ORM}`)}) */ ${column}` as unknown as AnyMySqlColumn<TPartial>;
|
|
764
776
|
}
|
|
765
|
-
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import {getHttpResponse, TriggerResponse} from "./index";
|
|
2
|
-
import {slowQueryPerHours} from "../utils/sqlUtils";
|
|
3
|
-
import {ForgeSqlOperation} from "../core/ForgeSQLQueryBuilder";
|
|
1
|
+
import { getHttpResponse, TriggerResponse } from "./index";
|
|
2
|
+
import { slowQueryPerHours } from "../utils/sqlUtils";
|
|
3
|
+
import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Scheduler trigger for analyzing slow queries from the last specified number of hours.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* This trigger analyzes slow queries from TiDB's slow query log system table and provides
|
|
9
9
|
* detailed performance information including SQL query text, memory usage, execution time,
|
|
10
10
|
* and execution plans. It's designed to be used as a scheduled trigger in Atlassian Forge
|
|
11
11
|
* to monitor query performance over time.
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* The function queries the slow query system table to find queries executed within the
|
|
14
14
|
* specified time window and logs detailed performance information to the console. Results
|
|
15
15
|
* are limited to the top 50 slow queries to prevent excessive output.
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* @param forgeSQLORM - The ForgeSQL operation instance for database access
|
|
18
18
|
* @param options - Configuration options for the slow query analysis
|
|
19
19
|
* @param options.hours - Number of hours to look back for slow queries (default: 1)
|
|
20
20
|
* @param options.timeout - Timeout in milliseconds for the query execution (default: 2000ms)
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
22
|
* @returns Promise<TriggerResponse<string>> - HTTP response with JSON stringified query results or error message
|
|
23
|
-
*
|
|
23
|
+
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```typescript
|
|
26
26
|
* import ForgeSQL, { slowQuerySchedulerTrigger } from "forge-sql-orm";
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
28
|
* const forgeSQL = new ForgeSQL();
|
|
29
|
-
*
|
|
29
|
+
*
|
|
30
30
|
* // Basic usage with default options (1 hour, 2000ms timeout)
|
|
31
31
|
* export const slowQueryTrigger = () =>
|
|
32
32
|
* slowQuerySchedulerTrigger(forgeSQL, { hours: 1, timeout: 2000 });
|
|
33
|
-
*
|
|
33
|
+
*
|
|
34
34
|
* // Analyze slow queries from the last 6 hours with extended timeout
|
|
35
35
|
* export const sixHourSlowQueryTrigger = () =>
|
|
36
36
|
* slowQuerySchedulerTrigger(forgeSQL, { hours: 6, timeout: 5000 });
|
|
37
|
-
*
|
|
37
|
+
*
|
|
38
38
|
* // Analyze slow queries from the last 24 hours
|
|
39
39
|
* export const dailySlowQueryTrigger = () =>
|
|
40
40
|
* slowQuerySchedulerTrigger(forgeSQL, { hours: 24, timeout: 3000 });
|
|
41
41
|
* ```
|
|
42
|
-
*
|
|
42
|
+
*
|
|
43
43
|
* @example
|
|
44
44
|
* ```yaml
|
|
45
45
|
* # manifest.yml configuration
|
|
@@ -47,36 +47,43 @@ import {ForgeSqlOperation} from "../core/ForgeSQLQueryBuilder";
|
|
|
47
47
|
* - key: slow-query-trigger
|
|
48
48
|
* function: slowQueryTrigger
|
|
49
49
|
* interval: hour
|
|
50
|
-
*
|
|
50
|
+
*
|
|
51
51
|
* function:
|
|
52
52
|
* - key: slowQueryTrigger
|
|
53
53
|
* handler: index.slowQueryTrigger
|
|
54
54
|
* ```
|
|
55
|
-
*
|
|
55
|
+
*
|
|
56
56
|
* @remarks
|
|
57
57
|
* - Results are automatically logged to the Forge Developer Console via `console.warn()`
|
|
58
58
|
* - The function returns up to 50 slow queries to prevent excessive logging
|
|
59
59
|
* - Transient timeouts are usually fine; repeated timeouts indicate the diagnostic query itself is slow
|
|
60
60
|
* - This trigger is best used with hourly intervals to catch slow queries in a timely manner
|
|
61
61
|
* - Error responses return HTTP 500 with error details
|
|
62
|
-
*
|
|
62
|
+
*
|
|
63
63
|
* @see {@link slowQueryPerHours} - The underlying function that performs the actual query analysis
|
|
64
64
|
*/
|
|
65
|
-
export async function slowQuerySchedulerTrigger(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
65
|
+
export async function slowQuerySchedulerTrigger(
|
|
66
|
+
forgeSQLORM: ForgeSqlOperation,
|
|
67
|
+
options: {
|
|
68
|
+
hours: number;
|
|
69
|
+
timeout: number;
|
|
70
|
+
},
|
|
71
|
+
): Promise<TriggerResponse<string>> {
|
|
72
|
+
try {
|
|
73
|
+
return getHttpResponse<string>(
|
|
74
|
+
200,
|
|
75
|
+
JSON.stringify(
|
|
76
|
+
await slowQueryPerHours(forgeSQLORM, options?.hours ?? 1, options?.timeout ?? 3000),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
const errorMessage =
|
|
81
|
+
error?.debug?.sqlMessage ??
|
|
82
|
+
error?.debug?.message ??
|
|
83
|
+
error.message ??
|
|
84
|
+
"Unknown error occurred";
|
|
85
|
+
// eslint-disable-next-line no-console
|
|
86
|
+
console.error(errorMessage);
|
|
87
|
+
return getHttpResponse<string>(500, errorMessage);
|
|
88
|
+
}
|
|
81
89
|
}
|
|
82
|
-
|