forge-sql-orm 2.1.23 → 2.1.25
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 +46 -23
- package/dist/core/ForgeSQLAnalyseOperations.d.ts +4 -0
- package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLAnalyseOperations.js +17 -21
- package/dist/core/ForgeSQLAnalyseOperations.js.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts +16 -0
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.js +60 -28
- package/dist/core/ForgeSQLCrudOperations.js.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +15 -28
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js +20 -47
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/Rovo.d.ts +32 -0
- package/dist/core/Rovo.d.ts.map +1 -1
- package/dist/core/Rovo.js +116 -67
- package/dist/core/Rovo.js.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.js +168 -118
- package/dist/lib/drizzle/extensions/additionalActions.js.map +1 -1
- package/dist/utils/cacheTableUtils.d.ts +0 -8
- package/dist/utils/cacheTableUtils.d.ts.map +1 -1
- package/dist/utils/cacheTableUtils.js +183 -126
- package/dist/utils/cacheTableUtils.js.map +1 -1
- package/dist/utils/cacheUtils.d.ts +13 -1
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.js +60 -47
- package/dist/utils/cacheUtils.js.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.js +31 -20
- package/dist/utils/forgeDriverProxy.js.map +1 -1
- package/dist/utils/sqlHints.d.ts.map +1 -1
- package/dist/utils/sqlHints.js +19 -29
- package/dist/utils/sqlHints.js.map +1 -1
- package/dist/utils/sqlUtils.d.ts +0 -29
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.js +107 -78
- package/dist/utils/sqlUtils.js.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +24 -4
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.js +24 -4
- package/dist/webtriggers/clearCacheSchedulerTrigger.js.map +1 -1
- package/package.json +9 -9
- package/src/core/ForgeSQLAnalyseOperations.ts +18 -21
- package/src/core/ForgeSQLCrudOperations.ts +83 -33
- package/src/core/ForgeSQLQueryBuilder.ts +59 -154
- package/src/core/Rovo.ts +158 -98
- package/src/lib/drizzle/extensions/additionalActions.ts +287 -382
- package/src/utils/cacheTableUtils.ts +202 -144
- package/src/utils/cacheUtils.ts +70 -47
- package/src/utils/forgeDriverProxy.ts +39 -21
- package/src/utils/sqlHints.ts +21 -26
- package/src/utils/sqlUtils.ts +151 -101
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +24 -4
|
@@ -16,6 +16,42 @@ const QUERY_ERROR_CODES = {
|
|
|
16
16
|
*/
|
|
17
17
|
const STATEMENTS_SUMMARY_DELAY_MS = 200;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Checks if error is a timeout or out-of-memory error.
|
|
21
|
+
*/
|
|
22
|
+
function isQueryError(error: any): { isTimeout: boolean; isOutOfMemory: boolean } {
|
|
23
|
+
const isTimeout = error?.code === QUERY_ERROR_CODES.TIMEOUT;
|
|
24
|
+
const isOutOfMemory = error?.context?.debug?.errno === QUERY_ERROR_CODES.OUT_OF_MEMORY_ERRNO;
|
|
25
|
+
return { isTimeout, isOutOfMemory };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handles timeout or out-of-memory errors by analyzing the query.
|
|
30
|
+
*/
|
|
31
|
+
async function handleQueryError(
|
|
32
|
+
queryStartTime: number,
|
|
33
|
+
forgeSqlOperation: ForgeSqlOperation,
|
|
34
|
+
isTimeout: boolean,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
// Wait for CLUSTER_STATEMENTS_SUMMARY to be populated with our failed query data
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, STATEMENTS_SUMMARY_DELAY_MS));
|
|
38
|
+
|
|
39
|
+
const queryEndTime = Date.now();
|
|
40
|
+
const queryDuration = queryEndTime - queryStartTime;
|
|
41
|
+
const errorType: "OOM" | "TIMEOUT" = isTimeout ? "TIMEOUT" : "OOM";
|
|
42
|
+
|
|
43
|
+
if (isTimeout) {
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.error(` TIMEOUT detected - Query exceeded time limit`);
|
|
46
|
+
} else {
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.error(`OUT OF MEMORY detected - Query exceeded memory limit`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Analyze the failed query using CLUSTER_STATEMENTS_SUMMARY
|
|
52
|
+
await handleErrorsWithPlan(forgeSqlOperation, queryDuration, errorType);
|
|
53
|
+
}
|
|
54
|
+
|
|
19
55
|
/**
|
|
20
56
|
* Creates a proxy for the forgeDriver that injects SQL hints and handles query analysis
|
|
21
57
|
* @param forgeSqlOperation - The ForgeSQL operation instance
|
|
@@ -51,28 +87,10 @@ export function createForgeDriverProxy(
|
|
|
51
87
|
// Execute the query with injected hints
|
|
52
88
|
return await forgeDriver(modifiedQuery, params, method);
|
|
53
89
|
} catch (error: any) {
|
|
54
|
-
|
|
55
|
-
const isTimeoutError = error.code === QUERY_ERROR_CODES.TIMEOUT;
|
|
56
|
-
const isOutOfMemoryError =
|
|
57
|
-
error?.context?.debug?.errno === QUERY_ERROR_CODES.OUT_OF_MEMORY_ERRNO;
|
|
58
|
-
|
|
59
|
-
if (isTimeoutError || isOutOfMemoryError) {
|
|
60
|
-
// Wait for CLUSTER_STATEMENTS_SUMMARY to be populated with our failed query data
|
|
61
|
-
await new Promise((resolve) => setTimeout(resolve, STATEMENTS_SUMMARY_DELAY_MS));
|
|
90
|
+
const { isTimeout, isOutOfMemory } = isQueryError(error);
|
|
62
91
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let errorType: "OOM" | "TIMEOUT" = "TIMEOUT";
|
|
66
|
-
if (isTimeoutError) {
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.error(` TIMEOUT detected - Query exceeded time limit`);
|
|
69
|
-
} else {
|
|
70
|
-
// eslint-disable-next-line no-console
|
|
71
|
-
console.error(`OUT OF MEMORY detected - Query exceeded memory limit`);
|
|
72
|
-
errorType = "OOM";
|
|
73
|
-
}
|
|
74
|
-
// Analyze the failed query using CLUSTER_STATEMENTS_SUMMARY
|
|
75
|
-
await handleErrorsWithPlan(forgeSqlOperation, queryDuration, errorType);
|
|
92
|
+
if (isTimeout || isOutOfMemory) {
|
|
93
|
+
await handleQueryError(queryStartTime, forgeSqlOperation, isTimeout);
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
// Log SQL error details if requested
|
package/src/utils/sqlHints.ts
CHANGED
|
@@ -26,38 +26,33 @@ export function injectSqlHints(query: string, hints?: SqlHints): string {
|
|
|
26
26
|
// Normalize the query for easier matching
|
|
27
27
|
const normalizedQuery = query.trim().toUpperCase();
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// Map query types to their hints
|
|
30
|
+
const queryTypeMap: Record<string, string[] | undefined> = {
|
|
31
|
+
SELECT: hints.select,
|
|
32
|
+
INSERT: hints.insert,
|
|
33
|
+
UPDATE: hints.update,
|
|
34
|
+
DELETE: hints.delete,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Find matching query type and get hints
|
|
30
38
|
let queryHints: string[] | undefined;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
queryHints = hints.delete;
|
|
39
|
+
let queryPrefix: string | null = null;
|
|
40
|
+
|
|
41
|
+
for (const [type, typeHints] of Object.entries(queryTypeMap)) {
|
|
42
|
+
if (normalizedQuery.startsWith(type)) {
|
|
43
|
+
queryPrefix = type;
|
|
44
|
+
queryHints = typeHints;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
// If no hints for this query type, return original query
|
|
43
|
-
if (!queryHints || queryHints.length === 0) {
|
|
50
|
+
if (!queryHints || queryHints.length === 0 || !queryPrefix) {
|
|
44
51
|
return query;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
// Join all hints with spaces
|
|
54
|
+
// Join all hints with spaces and inject into query
|
|
48
55
|
const hintsString = queryHints.join(" ");
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (normalizedQuery.startsWith("SELECT")) {
|
|
52
|
-
return `SELECT /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
53
|
-
} else if (normalizedQuery.startsWith("INSERT")) {
|
|
54
|
-
return `INSERT /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
55
|
-
} else if (normalizedQuery.startsWith("UPDATE")) {
|
|
56
|
-
return `UPDATE /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
57
|
-
} else if (normalizedQuery.startsWith("DELETE")) {
|
|
58
|
-
return `DELETE /*+ ${hintsString} */ ${query.substring(6)}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// If no match found, return original query
|
|
62
|
-
return query;
|
|
56
|
+
const prefixLength = queryPrefix.length;
|
|
57
|
+
return `${queryPrefix} /*+ ${hintsString} */ ${query.substring(prefixLength)}`;
|
|
63
58
|
}
|
package/src/utils/sqlUtils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// qlty-ignore: +qlty:file-complexity
|
|
1
2
|
import {
|
|
2
3
|
and,
|
|
3
4
|
AnyColumn,
|
|
@@ -60,40 +61,46 @@ interface ConfigBuilderData {
|
|
|
60
61
|
[key: string]: any;
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Parses a string value using multiple DateTime parsers.
|
|
66
|
+
*/
|
|
67
|
+
function parseStringToDate(value: string, format: string): Date {
|
|
68
|
+
// 1. Try to parse using the provided format (strict mode)
|
|
69
|
+
const dt = DateTime.fromFormat(value, format);
|
|
70
|
+
if (dt.isValid) {
|
|
71
|
+
return dt.toJSDate();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Try to parse as SQL string
|
|
75
|
+
const sqlDt = DateTime.fromSQL(value);
|
|
76
|
+
if (sqlDt.isValid) {
|
|
77
|
+
return sqlDt.toJSDate();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Try to parse as RFC2822 string
|
|
81
|
+
const isoDt = DateTime.fromRFC2822(value);
|
|
82
|
+
if (isoDt.isValid) {
|
|
83
|
+
return isoDt.toJSDate();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Fallback: use native Date constructor
|
|
87
|
+
return new Date(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
/**
|
|
64
91
|
* Parses a date string into a Date object using the specified format
|
|
65
92
|
* @param value - The date string to parse or Date
|
|
66
93
|
* @param format - The format to use for parsing
|
|
67
94
|
* @returns Date object
|
|
68
95
|
*/
|
|
69
|
-
|
|
70
96
|
export const parseDateTime = (value: string | Date, format: string): Date => {
|
|
71
97
|
let result: Date;
|
|
72
98
|
if (value instanceof Date) {
|
|
73
99
|
result = value;
|
|
74
100
|
} else {
|
|
75
|
-
|
|
76
|
-
const dt = DateTime.fromFormat(value, format);
|
|
77
|
-
if (dt.isValid) {
|
|
78
|
-
result = dt.toJSDate();
|
|
79
|
-
} else {
|
|
80
|
-
// 2. Try to parse as SQL string
|
|
81
|
-
const sqlDt = DateTime.fromSQL(value);
|
|
82
|
-
if (sqlDt.isValid) {
|
|
83
|
-
result = sqlDt.toJSDate();
|
|
84
|
-
} else {
|
|
85
|
-
// 3. Try to parse as RFC2822 string
|
|
86
|
-
const isoDt = DateTime.fromRFC2822(value);
|
|
87
|
-
if (isoDt.isValid) {
|
|
88
|
-
result = isoDt.toJSDate();
|
|
89
|
-
} else {
|
|
90
|
-
// 4. Fallback: use native Date constructor
|
|
91
|
-
result = new Date(value);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
101
|
+
result = parseStringToDate(value, format);
|
|
95
102
|
}
|
|
96
|
-
//
|
|
103
|
+
// Ensure the result is a valid Date object
|
|
97
104
|
if (Number.isNaN(result.getTime())) {
|
|
98
105
|
result = new Date(value);
|
|
99
106
|
}
|
|
@@ -667,38 +674,61 @@ export function applyFromDriverTransform<T, TSelection>(
|
|
|
667
674
|
});
|
|
668
675
|
}
|
|
669
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Checks if an object is a plain object (not Date, Array, etc.).
|
|
679
|
+
*/
|
|
680
|
+
function isPlainObject(obj: unknown): boolean {
|
|
681
|
+
return (
|
|
682
|
+
obj !== null &&
|
|
683
|
+
typeof obj === "object" &&
|
|
684
|
+
(!obj.constructor || obj.constructor.name === "Object")
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Processes a single value in the null branches processing.
|
|
690
|
+
*/
|
|
691
|
+
function processValueEntry(
|
|
692
|
+
key: string,
|
|
693
|
+
value: unknown,
|
|
694
|
+
result: Record<string, unknown>,
|
|
695
|
+
allNull: { value: boolean },
|
|
696
|
+
): void {
|
|
697
|
+
if (value === null || value === undefined) {
|
|
698
|
+
result[key] = null;
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (typeof value === "object" && isPlainObject(value)) {
|
|
703
|
+
const processed = processNullBranches(value as Record<string, unknown>);
|
|
704
|
+
result[key] = processed;
|
|
705
|
+
if (processed !== null) {
|
|
706
|
+
allNull.value = false;
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
result[key] = value;
|
|
710
|
+
allNull.value = false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
670
714
|
function processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {
|
|
671
715
|
if (obj === null || typeof obj !== "object") {
|
|
672
716
|
return obj;
|
|
673
717
|
}
|
|
674
718
|
|
|
675
719
|
// Skip built-in objects like Date, Array, etc.
|
|
676
|
-
if (obj
|
|
720
|
+
if (!isPlainObject(obj)) {
|
|
677
721
|
return obj;
|
|
678
722
|
}
|
|
679
723
|
|
|
680
724
|
const result: Record<string, unknown> = {};
|
|
681
|
-
|
|
725
|
+
const allNull = { value: true };
|
|
682
726
|
|
|
683
727
|
for (const [key, value] of Object.entries(obj)) {
|
|
684
|
-
|
|
685
|
-
result[key] = null;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (typeof value === "object") {
|
|
690
|
-
const processed = processNullBranches(value as Record<string, unknown>);
|
|
691
|
-
result[key] = processed;
|
|
692
|
-
if (processed !== null) {
|
|
693
|
-
allNull = false;
|
|
694
|
-
}
|
|
695
|
-
} else {
|
|
696
|
-
result[key] = value;
|
|
697
|
-
allNull = false;
|
|
698
|
-
}
|
|
728
|
+
processValueEntry(key, value, result, allNull);
|
|
699
729
|
}
|
|
700
730
|
|
|
701
|
-
return allNull ? null : result;
|
|
731
|
+
return allNull.value ? null : result;
|
|
702
732
|
}
|
|
703
733
|
|
|
704
734
|
export function formatLimitOffset(limitOrOffset: number): number {
|
|
@@ -745,6 +775,37 @@ function buildClusterStatementsSummaryQuery(forgeSQLORM: ForgeSqlOperation, time
|
|
|
745
775
|
);
|
|
746
776
|
}
|
|
747
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Formats and logs query performance result.
|
|
780
|
+
*/
|
|
781
|
+
function formatAndLogQueryResult(result: {
|
|
782
|
+
digestText: string;
|
|
783
|
+
avgLatency: number | bigint;
|
|
784
|
+
avgMem: number | bigint;
|
|
785
|
+
stmtType: string;
|
|
786
|
+
execCount: number | bigint;
|
|
787
|
+
plan: string | null;
|
|
788
|
+
}): void {
|
|
789
|
+
const avgTimeMs = Number(result.avgLatency) / 1_000_000;
|
|
790
|
+
const avgMemMB = Number(result.avgMem) / 1_000_000;
|
|
791
|
+
|
|
792
|
+
// eslint-disable-next-line no-console
|
|
793
|
+
console.warn(
|
|
794
|
+
`SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${Number(result.execCount)}\n Plan:${result.plan}`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Handles errors in query execution plan retrieval.
|
|
800
|
+
*/
|
|
801
|
+
function handleQueryPlanError(error: unknown): void {
|
|
802
|
+
// eslint-disable-next-line no-console
|
|
803
|
+
console.debug(
|
|
804
|
+
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
|
|
805
|
+
error,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
748
809
|
/**
|
|
749
810
|
* Analyzes and prints query performance data from CLUSTER_STATEMENTS_SUMMARY table.
|
|
750
811
|
*
|
|
@@ -785,22 +846,10 @@ export async function printQueriesWithPlan(
|
|
|
785
846
|
);
|
|
786
847
|
|
|
787
848
|
for (const result of results) {
|
|
788
|
-
|
|
789
|
-
const avgTimeMs = Number(result.avgLatency) / 1_000_000;
|
|
790
|
-
const avgMemMB = Number(result.avgMem) / 1_000_000;
|
|
791
|
-
|
|
792
|
-
// 1. Query info: SQL, memory, time, executions
|
|
793
|
-
// eslint-disable-next-line no-console
|
|
794
|
-
console.warn(
|
|
795
|
-
`SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${result.execCount}\n Plan:${result.plan}`,
|
|
796
|
-
);
|
|
849
|
+
formatAndLogQueryResult(result);
|
|
797
850
|
}
|
|
798
851
|
} catch (error) {
|
|
799
|
-
|
|
800
|
-
console.debug(
|
|
801
|
-
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
|
|
802
|
-
error,
|
|
803
|
-
);
|
|
852
|
+
handleQueryPlanError(error);
|
|
804
853
|
}
|
|
805
854
|
}
|
|
806
855
|
|
|
@@ -823,22 +872,10 @@ export async function handleErrorsWithPlan(
|
|
|
823
872
|
);
|
|
824
873
|
|
|
825
874
|
for (const result of results) {
|
|
826
|
-
|
|
827
|
-
const avgTimeMs = Number(result.avgLatency) / 1_000_000;
|
|
828
|
-
const avgMemMB = Number(result.avgMem) / 1_000_000;
|
|
829
|
-
|
|
830
|
-
// 1. Query info: SQL, memory, time, executions
|
|
831
|
-
// eslint-disable-next-line no-console
|
|
832
|
-
console.warn(
|
|
833
|
-
`SQL: ${result.digestText} | Memory: ${avgMemMB.toFixed(2)} MB | Time: ${avgTimeMs.toFixed(2)} ms | stmtType: ${result.stmtType} | Executions: ${result.execCount}\n Plan:${result.plan}`,
|
|
834
|
-
);
|
|
875
|
+
formatAndLogQueryResult(result);
|
|
835
876
|
}
|
|
836
877
|
} catch (error) {
|
|
837
|
-
|
|
838
|
-
console.debug(
|
|
839
|
-
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
|
|
840
|
-
error,
|
|
841
|
-
);
|
|
878
|
+
handleQueryPlanError(error);
|
|
842
879
|
}
|
|
843
880
|
}
|
|
844
881
|
|
|
@@ -873,6 +910,48 @@ const SESSION_ALIAS_NAME_ORM = "orm";
|
|
|
873
910
|
*
|
|
874
911
|
* @throws Does not throw - errors are logged to console.debug instead
|
|
875
912
|
*/
|
|
913
|
+
/**
|
|
914
|
+
* Builds slow query query for specified hours.
|
|
915
|
+
*/
|
|
916
|
+
function buildSlowQueryQuery(forgeSQLORM: ForgeSqlOperation, hours: number) {
|
|
917
|
+
return forgeSQLORM
|
|
918
|
+
.getDrizzleQueryBuilder()
|
|
919
|
+
.select({
|
|
920
|
+
query: withTidbHint(slowQuery.query),
|
|
921
|
+
queryTime: slowQuery.queryTime,
|
|
922
|
+
memMax: slowQuery.memMax,
|
|
923
|
+
plan: slowQuery.plan,
|
|
924
|
+
})
|
|
925
|
+
.from(slowQuery)
|
|
926
|
+
.where(
|
|
927
|
+
and(
|
|
928
|
+
isNotNull(slowQuery.digest),
|
|
929
|
+
ne(slowQuery.sessionAlias, SESSION_ALIAS_NAME_ORM),
|
|
930
|
+
gte(
|
|
931
|
+
slowQuery.time,
|
|
932
|
+
sql`DATE_SUB
|
|
933
|
+
(NOW(), INTERVAL
|
|
934
|
+
${hours}
|
|
935
|
+
HOUR
|
|
936
|
+
)`,
|
|
937
|
+
),
|
|
938
|
+
),
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Formats slow query result message.
|
|
944
|
+
*/
|
|
945
|
+
function formatSlowQueryMessage(result: {
|
|
946
|
+
query: string | null;
|
|
947
|
+
queryTime: number | null;
|
|
948
|
+
memMax: number | bigint | null;
|
|
949
|
+
plan: string | null;
|
|
950
|
+
}): string {
|
|
951
|
+
const memMaxMB = result.memMax ? Number(result.memMax) / 1_000_000 : 0;
|
|
952
|
+
return `Found SlowQuery SQL: ${result.query} | Memory: ${memMaxMB.toFixed(2)} MB | Time: ${result.queryTime} ms\n Plan:${result.plan}`;
|
|
953
|
+
}
|
|
954
|
+
|
|
876
955
|
export async function slowQueryPerHours(
|
|
877
956
|
forgeSQLORM: ForgeSqlOperation,
|
|
878
957
|
hours: number,
|
|
@@ -880,51 +959,22 @@ export async function slowQueryPerHours(
|
|
|
880
959
|
) {
|
|
881
960
|
try {
|
|
882
961
|
const timeoutMs = timeout ?? 1500;
|
|
962
|
+
const query = buildSlowQueryQuery(forgeSQLORM, hours);
|
|
883
963
|
const results = await withTimeout(
|
|
884
|
-
|
|
885
|
-
.getDrizzleQueryBuilder()
|
|
886
|
-
.select({
|
|
887
|
-
query: withTidbHint(slowQuery.query),
|
|
888
|
-
queryTime: slowQuery.queryTime,
|
|
889
|
-
memMax: slowQuery.memMax,
|
|
890
|
-
plan: slowQuery.plan,
|
|
891
|
-
})
|
|
892
|
-
.from(slowQuery)
|
|
893
|
-
.where(
|
|
894
|
-
and(
|
|
895
|
-
isNotNull(slowQuery.digest),
|
|
896
|
-
ne(slowQuery.sessionAlias, SESSION_ALIAS_NAME_ORM),
|
|
897
|
-
gte(
|
|
898
|
-
slowQuery.time,
|
|
899
|
-
sql`DATE_SUB
|
|
900
|
-
(NOW(), INTERVAL
|
|
901
|
-
${hours}
|
|
902
|
-
HOUR
|
|
903
|
-
)`,
|
|
904
|
-
),
|
|
905
|
-
),
|
|
906
|
-
),
|
|
964
|
+
query,
|
|
907
965
|
`Timeout ${timeoutMs}ms in slowQueryPerHours - transient timeouts are usually fine; repeated timeouts mean this diagnostic query is consistently slow and should be investigated`,
|
|
908
966
|
timeoutMs,
|
|
909
967
|
);
|
|
910
968
|
const response: string[] = [];
|
|
911
969
|
for (const result of results) {
|
|
912
|
-
|
|
913
|
-
const memMaxMB = result.memMax ? Number(result.memMax) / 1_000_000 : 0;
|
|
914
|
-
|
|
915
|
-
const message = `Found SlowQuery SQL: ${result.query} | Memory: ${memMaxMB.toFixed(2)} MB | Time: ${result.queryTime} ms\n Plan:${result.plan}`;
|
|
970
|
+
const message = formatSlowQueryMessage(result);
|
|
916
971
|
response.push(message);
|
|
917
|
-
// 1. Query info: SQL, memory, time, executions
|
|
918
972
|
// eslint-disable-next-line no-console
|
|
919
973
|
console.warn(message);
|
|
920
974
|
}
|
|
921
975
|
return response;
|
|
922
976
|
} catch (error) {
|
|
923
|
-
|
|
924
|
-
console.debug(
|
|
925
|
-
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}. Try again after some time`,
|
|
926
|
-
error,
|
|
927
|
-
);
|
|
977
|
+
handleQueryPlanError(error);
|
|
928
978
|
return [
|
|
929
979
|
`Error occurred while retrieving query execution plan: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
930
980
|
];
|
|
@@ -2,10 +2,28 @@ import { clearExpiredCache } from "../utils/cacheUtils";
|
|
|
2
2
|
import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Scheduler trigger for clearing expired cache entries.
|
|
5
|
+
* Scheduler trigger for proactively clearing expired cache entries.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* **Why this trigger is needed:**
|
|
8
|
+
*
|
|
9
|
+
* While forge-sql-orm uses Forge KVS TTL feature to mark entries as expired, **actual deletion
|
|
10
|
+
* is asynchronous and may take up to 48 hours**. During this window, expired entries remain in
|
|
11
|
+
* storage and can impact INSERT/UPDATE performance if the cache grows large.
|
|
12
|
+
*
|
|
13
|
+
* This scheduler trigger proactively cleans up expired entries by querying the expiration index
|
|
14
|
+
* and deleting entries where expiration < now, preventing cache growth from impacting data
|
|
15
|
+
* modification operations.
|
|
16
|
+
*
|
|
17
|
+
* **When to use:**
|
|
18
|
+
* - Your cache grows large over time
|
|
19
|
+
* - INSERT/UPDATE operations are slowing down due to cache size
|
|
20
|
+
* - You need strict expiry semantics (immediate cleanup)
|
|
21
|
+
* - You want to reduce storage costs proactively
|
|
22
|
+
*
|
|
23
|
+
* **When optional:**
|
|
24
|
+
* - Small cache footprint
|
|
25
|
+
* - No performance impact on data modifications
|
|
26
|
+
* - You can tolerate expired entries being returned for up to 48 hours
|
|
9
27
|
*
|
|
10
28
|
* @note This function is automatically disabled in production environments and will return a 500 error if called.
|
|
11
29
|
*
|
|
@@ -28,7 +46,7 @@ import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
|
|
|
28
46
|
*
|
|
29
47
|
* @example
|
|
30
48
|
* ```yaml
|
|
31
|
-
* # In manifest.yml
|
|
49
|
+
* # In manifest.yml (optional - only if cache growth impacts INSERT/UPDATE performance)
|
|
32
50
|
* scheduledTrigger:
|
|
33
51
|
* - key: clear-cache-trigger
|
|
34
52
|
* function: clearCache
|
|
@@ -38,6 +56,8 @@ import { ForgeSqlOrmOptions } from "../core/ForgeSQLQueryBuilder";
|
|
|
38
56
|
* - key: clearCache
|
|
39
57
|
* handler: index.clearCache
|
|
40
58
|
* ```
|
|
59
|
+
*
|
|
60
|
+
* @see https://developer.atlassian.com/platform/forge/runtime-reference/storage-api-basic-api/#ttl
|
|
41
61
|
*/
|
|
42
62
|
export const clearCacheSchedulerTrigger = async (options?: ForgeSqlOrmOptions) => {
|
|
43
63
|
try {
|