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.
- package/README.md +202 -254
- package/dist/ForgeSQLORM.js +3238 -3231
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +3236 -3229
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +70 -16
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +95 -16
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/SystemTables.d.ts +82 -82
- package/dist/lib/drizzle/extensions/additionalActions.d.ts +30 -6
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts +6 -2
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +5 -2
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/sqlUtils.d.ts +72 -1
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/index.d.ts +1 -1
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts +67 -0
- package/dist/webtriggers/slowQuerySchedulerTrigger.d.ts.map +1 -0
- package/package.json +14 -14
- package/src/core/ForgeSQLORM.ts +165 -34
- package/src/core/ForgeSQLQueryBuilder.ts +118 -19
- package/src/core/SystemTables.ts +1 -1
- package/src/lib/drizzle/extensions/additionalActions.ts +231 -18
- package/src/utils/cacheUtils.ts +3 -1
- package/src/utils/forgeDriver.ts +10 -42
- package/src/utils/forgeDriverProxy.ts +58 -6
- package/src/utils/metadataContextUtils.ts +21 -6
- package/src/utils/sqlUtils.ts +229 -2
- package/src/webtriggers/index.ts +1 -1
- package/src/webtriggers/slowQuerySchedulerTrigger.ts +82 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +0 -114
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +0 -1
- 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
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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>(
|
|
778
|
-
|
|
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>(
|
|
794
|
-
|
|
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>(
|
|
809
|
-
|
|
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>(
|
|
825
|
-
|
|
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
|
// ============================================================================
|
package/src/utils/cacheUtils.ts
CHANGED
|
@@ -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 &&
|
package/src/utils/forgeDriver.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
45
|
+
console.debug(`SQL Hints injected: ${modifiedQuery}`);
|
|
24
46
|
}
|
|
47
|
+
|
|
48
|
+
const queryStartTime = Date.now();
|
|
49
|
+
|
|
25
50
|
try {
|
|
26
|
-
//
|
|
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(
|
|
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
|
-
|
|
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
|
|
15
|
+
export async function saveMetaDataToContext(metadata?: ForgeSQLMetadata): Promise<void> {
|
|
12
16
|
const context = metadataQueryContext.getStore();
|
|
13
|
-
if (context
|
|
14
|
-
context.
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|