forge-sql-orm 2.1.9 → 2.1.11

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.
Files changed (39) hide show
  1. package/README.md +202 -254
  2. package/dist/ForgeSQLORM.js +3238 -3231
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +3236 -3229
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLORM.d.ts +70 -16
  7. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLQueryBuilder.d.ts +95 -16
  9. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  10. package/dist/core/SystemTables.d.ts +82 -82
  11. package/dist/lib/drizzle/extensions/additionalActions.d.ts +30 -6
  12. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
  13. package/dist/utils/cacheUtils.d.ts.map +1 -1
  14. package/dist/utils/forgeDriver.d.ts.map +1 -1
  15. package/dist/utils/forgeDriverProxy.d.ts +6 -2
  16. package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
  17. package/dist/utils/metadataContextUtils.d.ts +5 -2
  18. package/dist/utils/metadataContextUtils.d.ts.map +1 -1
  19. package/dist/utils/sqlUtils.d.ts +72 -1
  20. package/dist/utils/sqlUtils.d.ts.map +1 -1
  21. package/dist/webtriggers/index.d.ts +1 -1
  22. package/dist/webtriggers/index.d.ts.map +1 -1
  23. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts +67 -0
  24. package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -0
  25. package/package.json +14 -14
  26. package/src/core/ForgeSQLORM.ts +165 -34
  27. package/src/core/ForgeSQLQueryBuilder.ts +118 -19
  28. package/src/core/SystemTables.ts +1 -1
  29. package/src/lib/drizzle/extensions/additionalActions.ts +231 -18
  30. package/src/utils/cacheUtils.ts +3 -1
  31. package/src/utils/forgeDriver.ts +10 -42
  32. package/src/utils/forgeDriverProxy.ts +58 -6
  33. package/src/utils/metadataContextUtils.ts +21 -6
  34. package/src/utils/sqlUtils.ts +229 -2
  35. package/src/webtriggers/index.ts +1 -1
  36. package/src/webtriggers/slowQuerySchedulerTrigger.ts +82 -0
  37. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +0 -114
  38. package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +0 -1
  39. package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +0 -563
@@ -8,7 +8,7 @@ import {
8
8
  import { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
9
9
  import { applyFromDriverTransform, ForgeSqlOrmOptions, mapSelectFieldsWithAlias } from "../../..";
10
10
  import { MySqlSelectBase, MySqlSelectBuilder } from "drizzle-orm/mysql-core";
11
- import type { MySqlTable } from "drizzle-orm/mysql-core/table";
11
+ import { MySqlTable } from "drizzle-orm/mysql-core/table";
12
12
  import {
13
13
  MySqlDeleteBase,
14
14
  MySqlInsertBuilder,
@@ -26,9 +26,10 @@ import { isSQLWrapper, SQLWrapper } from "drizzle-orm/sql/sql";
26
26
  import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
27
27
  import { getTableColumns, Query, SQL } from "drizzle-orm";
28
28
  import { MySqlDialect } from "drizzle-orm/mysql-core/dialect";
29
- import type {
29
+ import {
30
30
  GetSelectTableName,
31
31
  GetSelectTableSelection,
32
+ SelectResultField,
32
33
  } from "drizzle-orm/query-builders/select.types";
33
34
 
34
35
  // ============================================================================
@@ -142,12 +143,19 @@ export type SelectAllFromAliasedType = <T extends MySqlTable>(
142
143
  table: T,
143
144
  ) => MySqlSelectBase<
144
145
  GetSelectTableName<T>,
145
- T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"],
146
- T["_"]["columns"] extends undefined ? "single" : "partial",
146
+ GetSelectTableSelection<T>,
147
+ "single",
147
148
  MySqlRemotePreparedQueryHKT,
148
149
  GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
149
150
  false,
150
151
  never,
152
+ {
153
+ [K in keyof {
154
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
155
+ }]: {
156
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
157
+ }[K];
158
+ }[],
151
159
  any
152
160
  >;
153
161
 
@@ -158,12 +166,19 @@ export type SelectAllDistinctFromAliasedType = <T extends MySqlTable>(
158
166
  table: T,
159
167
  ) => MySqlSelectBase<
160
168
  GetSelectTableName<T>,
161
- T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"],
162
- T["_"]["columns"] extends undefined ? "single" : "partial",
169
+ GetSelectTableSelection<T>,
170
+ "single",
163
171
  MySqlRemotePreparedQueryHKT,
164
172
  GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
165
173
  false,
166
174
  never,
175
+ {
176
+ [K in keyof {
177
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
178
+ }]: {
179
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
180
+ }[K];
181
+ }[],
167
182
  any
168
183
  >;
169
184
 
@@ -175,12 +190,19 @@ export type SelectAllFromCacheableAliasedType = <T extends MySqlTable>(
175
190
  cacheTtl?: number,
176
191
  ) => MySqlSelectBase<
177
192
  GetSelectTableName<T>,
178
- T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"],
179
- T["_"]["columns"] extends undefined ? "single" : "partial",
193
+ GetSelectTableSelection<T>,
194
+ "single",
180
195
  MySqlRemotePreparedQueryHKT,
181
196
  GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
182
197
  false,
183
198
  never,
199
+ {
200
+ [K in keyof {
201
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
202
+ }]: {
203
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
204
+ }[K];
205
+ }[],
184
206
  any
185
207
  >;
186
208
 
@@ -192,12 +214,19 @@ export type SelectAllDistinctFromCacheableAliasedType = <T extends MySqlTable>(
192
214
  cacheTtl?: number,
193
215
  ) => MySqlSelectBase<
194
216
  GetSelectTableName<T>,
195
- T["_"]["columns"] extends undefined ? GetSelectTableSelection<T> : T["_"]["columns"],
196
- T["_"]["columns"] extends undefined ? "single" : "partial",
217
+ GetSelectTableSelection<T>,
218
+ "single",
197
219
  MySqlRemotePreparedQueryHKT,
198
220
  GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
199
221
  false,
200
222
  never,
223
+ {
224
+ [K in keyof {
225
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
226
+ }]: {
227
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
228
+ }[K];
229
+ }[],
201
230
  any
202
231
  >;
203
232
 
@@ -774,8 +803,52 @@ export function patchDbWithSelectAliased(
774
803
  * const users = await db.selectFrom(userTable).where(eq(userTable.id, 1));
775
804
  * ```
776
805
  */
777
- db.selectFrom = function <T extends MySqlTable>(table: T) {
778
- return db.selectAliased(getTableColumns(table)).from(table);
806
+ db.selectFrom = function <T extends MySqlTable>(
807
+ table: T,
808
+ ): MySqlSelectBase<
809
+ GetSelectTableName<T>,
810
+ GetSelectTableSelection<T>,
811
+ "single",
812
+ MySqlRemotePreparedQueryHKT,
813
+ GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
814
+ false,
815
+ never,
816
+ {
817
+ [K in keyof {
818
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
819
+ GetSelectTableSelection<T>[Key]
820
+ >;
821
+ }]: {
822
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
823
+ GetSelectTableSelection<T>[Key]
824
+ >;
825
+ }[K];
826
+ }[],
827
+ any
828
+ > {
829
+ return db.selectAliased(getTableColumns(table)).from(table) as unknown as MySqlSelectBase<
830
+ GetSelectTableName<T>,
831
+ GetSelectTableSelection<T>,
832
+ "single",
833
+ MySqlRemotePreparedQueryHKT,
834
+ GetSelectTableName<T> extends string
835
+ ? Record<string & GetSelectTableName<T>, "not-null">
836
+ : {},
837
+ false,
838
+ never,
839
+ {
840
+ [K in keyof {
841
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
842
+ GetSelectTableSelection<T>[Key]
843
+ >;
844
+ }]: {
845
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
846
+ GetSelectTableSelection<T>[Key]
847
+ >;
848
+ }[K];
849
+ }[],
850
+ any
851
+ >;
779
852
  };
780
853
 
781
854
  /**
@@ -790,8 +863,55 @@ export function patchDbWithSelectAliased(
790
863
  * const users = await db.selectFromCacheable(userTable, 300).where(eq(userTable.id, 1));
791
864
  * ```
792
865
  */
793
- db.selectFromCacheable = function <T extends MySqlTable>(table: T, cacheTtl?: number) {
794
- return db.selectAliasedCacheable(getTableColumns(table), cacheTtl).from(table);
866
+ db.selectFromCacheable = function <T extends MySqlTable>(
867
+ table: T,
868
+ cacheTtl?: number,
869
+ ): MySqlSelectBase<
870
+ GetSelectTableName<T>,
871
+ GetSelectTableSelection<T>,
872
+ "single",
873
+ MySqlRemotePreparedQueryHKT,
874
+ GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
875
+ false,
876
+ never,
877
+ {
878
+ [K in keyof {
879
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
880
+ GetSelectTableSelection<T>[Key]
881
+ >;
882
+ }]: {
883
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
884
+ GetSelectTableSelection<T>[Key]
885
+ >;
886
+ }[K];
887
+ }[],
888
+ any
889
+ > {
890
+ return db
891
+ .selectAliasedCacheable(getTableColumns(table), cacheTtl)
892
+ .from(table) as unknown as MySqlSelectBase<
893
+ GetSelectTableName<T>,
894
+ GetSelectTableSelection<T>,
895
+ "single",
896
+ MySqlRemotePreparedQueryHKT,
897
+ GetSelectTableName<T> extends string
898
+ ? Record<string & GetSelectTableName<T>, "not-null">
899
+ : {},
900
+ false,
901
+ never,
902
+ {
903
+ [K in keyof {
904
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
905
+ GetSelectTableSelection<T>[Key]
906
+ >;
907
+ }]: {
908
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
909
+ GetSelectTableSelection<T>[Key]
910
+ >;
911
+ }[K];
912
+ }[],
913
+ any
914
+ >;
795
915
  };
796
916
 
797
917
  /**
@@ -805,8 +925,54 @@ export function patchDbWithSelectAliased(
805
925
  * const uniqueUsers = await db.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
806
926
  * ```
807
927
  */
808
- db.selectDistinctFrom = function <T extends MySqlTable>(table: T) {
809
- return db.selectAliasedDistinct(getTableColumns(table)).from(table);
928
+ db.selectDistinctFrom = function <T extends MySqlTable>(
929
+ table: T,
930
+ ): MySqlSelectBase<
931
+ GetSelectTableName<T>,
932
+ GetSelectTableSelection<T>,
933
+ "single",
934
+ MySqlRemotePreparedQueryHKT,
935
+ GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
936
+ false,
937
+ never,
938
+ {
939
+ [K in keyof {
940
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
941
+ GetSelectTableSelection<T>[Key]
942
+ >;
943
+ }]: {
944
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
945
+ GetSelectTableSelection<T>[Key]
946
+ >;
947
+ }[K];
948
+ }[],
949
+ any
950
+ > {
951
+ return db
952
+ .selectAliasedDistinct(getTableColumns(table))
953
+ .from(table) as unknown as MySqlSelectBase<
954
+ GetSelectTableName<T>,
955
+ GetSelectTableSelection<T>,
956
+ "single",
957
+ MySqlRemotePreparedQueryHKT,
958
+ GetSelectTableName<T> extends string
959
+ ? Record<string & GetSelectTableName<T>, "not-null">
960
+ : {},
961
+ false,
962
+ never,
963
+ {
964
+ [K in keyof {
965
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
966
+ GetSelectTableSelection<T>[Key]
967
+ >;
968
+ }]: {
969
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
970
+ GetSelectTableSelection<T>[Key]
971
+ >;
972
+ }[K];
973
+ }[],
974
+ any
975
+ >;
810
976
  };
811
977
 
812
978
  /**
@@ -821,8 +987,55 @@ export function patchDbWithSelectAliased(
821
987
  * const uniqueUsers = await db.selectDistinctFromCacheable(userTable, 300).where(eq(userTable.status, 'active'));
822
988
  * ```
823
989
  */
824
- db.selectDistinctFromCacheable = function <T extends MySqlTable>(table: T, cacheTtl?: number) {
825
- return db.selectAliasedDistinctCacheable(getTableColumns(table), cacheTtl).from(table);
990
+ db.selectDistinctFromCacheable = function <T extends MySqlTable>(
991
+ table: T,
992
+ cacheTtl?: number,
993
+ ): MySqlSelectBase<
994
+ GetSelectTableName<T>,
995
+ GetSelectTableSelection<T>,
996
+ "single",
997
+ MySqlRemotePreparedQueryHKT,
998
+ GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
999
+ false,
1000
+ never,
1001
+ {
1002
+ [K in keyof {
1003
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
1004
+ GetSelectTableSelection<T>[Key]
1005
+ >;
1006
+ }]: {
1007
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
1008
+ GetSelectTableSelection<T>[Key]
1009
+ >;
1010
+ }[K];
1011
+ }[],
1012
+ any
1013
+ > {
1014
+ return db
1015
+ .selectAliasedDistinctCacheable(getTableColumns(table), cacheTtl)
1016
+ .from(table) as unknown as MySqlSelectBase<
1017
+ GetSelectTableName<T>,
1018
+ GetSelectTableSelection<T>,
1019
+ "single",
1020
+ MySqlRemotePreparedQueryHKT,
1021
+ GetSelectTableName<T> extends string
1022
+ ? Record<string & GetSelectTableName<T>, "not-null">
1023
+ : {},
1024
+ false,
1025
+ never,
1026
+ {
1027
+ [K in keyof {
1028
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
1029
+ GetSelectTableSelection<T>[Key]
1030
+ >;
1031
+ }]: {
1032
+ [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
1033
+ GetSelectTableSelection<T>[Key]
1034
+ >;
1035
+ }[K];
1036
+ }[],
1037
+ any
1038
+ >;
826
1039
  };
827
1040
 
828
1041
  // ============================================================================
@@ -332,7 +332,9 @@ export async function getFromCache<T>(
332
332
  }
333
333
 
334
334
  try {
335
- const cacheResult = await kvs.entity<CacheEntity>(options.cacheEntityName).get(key);
335
+ const cacheResult = (await kvs.entity<CacheEntity>(options.cacheEntityName).get(key)) as
336
+ | CacheEntity
337
+ | undefined;
336
338
 
337
339
  if (
338
340
  cacheResult &&
@@ -1,6 +1,10 @@
1
1
  import { sql, UpdateQueryResponse } from "@forge/sql";
2
2
  import { saveMetaDataToContext } from "./metadataContextUtils";
3
3
  import { getOperationType } from "./requestTypeContextUtils";
4
+ import {withTimeout} from "./sqlUtils";
5
+
6
+ const timeoutMs =10000;
7
+ const timeoutMessage = `Atlassian @forge/sql did not return a response within ${timeoutMs}ms (${timeoutMs / 1000} seconds), so the request is blocked. Possible causes: slow query, network issues, or exceeding Forge SQL limits.`;
4
8
 
5
9
  /**
6
10
  * Metadata structure for Forge SQL query results.
@@ -62,36 +66,6 @@ export function isUpdateQueryResponse(obj: unknown): obj is UpdateQueryResponse
62
66
  );
63
67
  }
64
68
 
65
- /**
66
- * Executes a promise with a timeout.
67
- *
68
- * @param promise - The promise to execute
69
- * @param timeoutMs - Timeout in milliseconds (default: 10000ms)
70
- * @returns Promise that resolves with the result or rejects on timeout
71
- * @throws {Error} When the operation times out
72
- */
73
- async function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> {
74
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
75
-
76
- const timeoutPromise = new Promise<never>((_, reject) => {
77
- timeoutId = setTimeout(() => {
78
- reject(
79
- new Error(
80
- `Atlassian @forge/sql did not return a response within ${timeoutMs}ms (${timeoutMs / 1000} seconds), so the request is blocked. Possible causes: slow query, network issues, or exceeding Forge SQL limits.`,
81
- ),
82
- );
83
- }, timeoutMs);
84
- });
85
-
86
- try {
87
- return await Promise.race([promise, timeoutPromise]);
88
- } finally {
89
- if (timeoutId) {
90
- clearTimeout(timeoutId);
91
- }
92
- }
93
- }
94
-
95
69
  function inlineParams(sql: string, params: unknown[]): string {
96
70
  let i = 0;
97
71
  return sql.replace(/\?/g, () => {
@@ -124,7 +98,7 @@ async function processDDLResult(method: QueryMethod, result: any): Promise<Forge
124
98
 
125
99
  if (Array.isArray(result.rows)) {
126
100
  if (method === "execute") {
127
- return { rows: result.rows };
101
+ return { rows: [result.rows] };
128
102
  } else {
129
103
  const rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));
130
104
  return { rows };
@@ -148,17 +122,12 @@ async function processExecuteMethod(query: string, params: unknown[]): Promise<F
148
122
  sqlStatement.bindParams(...params);
149
123
  }
150
124
 
151
- const result = await withTimeout(sqlStatement.execute());
125
+ const result = await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs);
152
126
  await saveMetaDataToContext(result.metadata as ForgeSQLMetadata);
153
- if (!result?.rows) {
127
+ if (!result.rows) {
154
128
  return { rows: [[]] };
155
129
  }
156
130
 
157
- if (isUpdateQueryResponse(result.rows)) {
158
- const oneRow = result.rows as any;
159
- return { rows: [oneRow] };
160
- }
161
-
162
131
  return { rows: [result.rows] };
163
132
  }
164
133
 
@@ -176,10 +145,10 @@ async function processAllMethod(query: string, params: unknown[]): Promise<Forge
176
145
  await sqlStatement.bindParams(...params);
177
146
  }
178
147
 
179
- const result = (await withTimeout(sqlStatement.execute())) as ForgeSQLResult;
148
+ const result = (await withTimeout(sqlStatement.execute(), timeoutMessage, timeoutMs)) as ForgeSQLResult;
180
149
  await saveMetaDataToContext(result.metadata);
181
150
 
182
- if (!result?.rows) {
151
+ if (!result.rows) {
183
152
  return { rows: [] };
184
153
  }
185
154
 
@@ -217,10 +186,9 @@ export const forgeDriver = async (
217
186
  method: QueryMethod,
218
187
  ): Promise<ForgeDriverResult> => {
219
188
  const operationType = await getOperationType();
220
-
221
189
  // Handle DDL operations
222
190
  if (operationType === "DDL") {
223
- const result = await withTimeout(sql.executeDDL(inlineParams(query, params)));
191
+ const result = await withTimeout(sql.executeDDL(inlineParams(query, params)), timeoutMessage, timeoutMs);
224
192
  return await processDDLResult(method, result);
225
193
  }
226
194
 
@@ -1,11 +1,33 @@
1
1
  import { forgeDriver } from "./forgeDriver";
2
2
  import { injectSqlHints, SqlHints } from "./sqlHints";
3
+ import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
4
+ import { printQueriesWithPlan } from "./sqlUtils";
3
5
 
4
6
  /**
5
- * Creates a proxy for the forgeDriver that injects SQL hints
7
+ * Error codes and constants for query analysis
8
+ */
9
+ const QUERY_ERROR_CODES = {
10
+ TIMEOUT: "SQL_QUERY_TIMEOUT",
11
+ OUT_OF_MEMORY_ERRNO: 8175,
12
+ } as const;
13
+
14
+ /**
15
+ * Delay to wait for CLUSTER_STATEMENTS_SUMMARY to be populated
16
+ */
17
+ const STATEMENTS_SUMMARY_DELAY_MS = 200;
18
+
19
+ /**
20
+ * Creates a proxy for the forgeDriver that injects SQL hints and handles query analysis
21
+ * @param forgeSqlOperation - The ForgeSQL operation instance
22
+ * @param options - SQL hints to inject
23
+ * @param logRawSqlQuery - Whether to log raw SQL queries
6
24
  * @returns A proxied version of the forgeDriver
7
25
  */
8
- export function createForgeDriverProxy(options?: SqlHints, logRawSqlQuery?: boolean) {
26
+ export function createForgeDriverProxy(
27
+ forgeSqlOperation: ForgeSqlOperation,
28
+ options?: SqlHints,
29
+ logRawSqlQuery?: boolean,
30
+ ) {
9
31
  return async (
10
32
  query: string,
11
33
  params: any[],
@@ -20,16 +42,46 @@ export function createForgeDriverProxy(options?: SqlHints, logRawSqlQuery?: bool
20
42
 
21
43
  if (options && logRawSqlQuery && modifiedQuery !== query) {
22
44
  // eslint-disable-next-line no-console
23
- console.debug("injected Hints: " + modifiedQuery);
45
+ console.debug(`SQL Hints injected: ${modifiedQuery}`);
24
46
  }
47
+
48
+ const queryStartTime = Date.now();
49
+
25
50
  try {
26
- // Call the original forgeDriver with the modified query
51
+ // Execute the query with injected hints
27
52
  return await forgeDriver(modifiedQuery, params, method);
28
- } catch (error) {
53
+ } catch (error: any) {
54
+ // Check if this is a timeout or out-of-memory error that we want to analyze
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
+ if (isTimeoutError) {
61
+ // eslint-disable-next-line no-console
62
+ console.error(` TIMEOUT detected - Query exceeded time limit`);
63
+ } else {
64
+ // eslint-disable-next-line no-console
65
+ console.error(`OUT OF MEMORY detected - Query exceeded memory limit`);
66
+ }
67
+
68
+ // Wait for CLUSTER_STATEMENTS_SUMMARY to be populated with our failed query data
69
+ await new Promise((resolve) => setTimeout(resolve, STATEMENTS_SUMMARY_DELAY_MS));
70
+
71
+ const queryEndTime = Date.now();
72
+ const queryDuration = queryEndTime - queryStartTime;
73
+
74
+ // Analyze the failed query using CLUSTER_STATEMENTS_SUMMARY
75
+ await printQueriesWithPlan(forgeSqlOperation, queryDuration);
76
+ }
77
+
78
+ // Log SQL error details if requested
29
79
  if (logRawSqlQuery) {
30
80
  // eslint-disable-next-line no-console
31
- console.debug("SQL Error:", JSON.stringify(error));
81
+ console.debug(`SQL Error Details:`, JSON.stringify(error, null, 2));
32
82
  }
83
+
84
+ // Re-throw the original error
33
85
  throw error;
34
86
  }
35
87
  };
@@ -1,19 +1,34 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { ForgeSQLMetadata } from "./forgeDriver";
3
+ import { ForgeSqlOperation } from "../core/ForgeSQLQueryBuilder";
4
+ import { printQueriesWithPlan } from "./sqlUtils";
3
5
 
4
6
  export type MetadataQueryContext = {
5
7
  totalDbExecutionTime: number;
6
8
  totalResponseSize: number;
7
- lastMetadata?: ForgeSQLMetadata;
9
+ beginTime: Date;
10
+ printQueriesWithPlan: () => Promise<void>;
11
+ forgeSQLORM: ForgeSqlOperation;
8
12
  };
9
13
  export const metadataQueryContext = new AsyncLocalStorage<MetadataQueryContext>();
10
14
 
11
- export async function saveMetaDataToContext(metadata: ForgeSQLMetadata): Promise<void> {
15
+ export async function saveMetaDataToContext(metadata?: ForgeSQLMetadata): Promise<void> {
12
16
  const context = metadataQueryContext.getStore();
13
- if (context && metadata) {
14
- context.totalResponseSize += metadata.responseSize;
15
- context.totalDbExecutionTime += metadata.dbExecutionTime;
16
- context.lastMetadata = metadata;
17
+ if (context) {
18
+ context.printQueriesWithPlan = async () => {
19
+ if (process.env.NODE_ENV !== "test") {
20
+ await new Promise((r) => setTimeout(r, 200));
21
+ }
22
+ await printQueriesWithPlan(
23
+ context.forgeSQLORM,
24
+ Date.now() - context.beginTime.getTime(),
25
+ );
26
+ };
27
+ if (metadata) {
28
+ context.totalResponseSize += metadata.responseSize;
29
+ context.totalDbExecutionTime += metadata.dbExecutionTime;
30
+ }
31
+ // Log the results to console
17
32
  }
18
33
  }
19
34