forge-sql-orm 2.1.4 → 2.1.5
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 +91 -5
- package/dist/ForgeSQLORM.js +122 -17
- package/dist/ForgeSQLORM.js.map +1 -1
- package/dist/ForgeSQLORM.mjs +122 -17
- package/dist/ForgeSQLORM.mjs.map +1 -1
- package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.d.ts +23 -0
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +36 -5
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLSelectOperations.d.ts.map +1 -1
- package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -1
- package/dist/utils/cacheContextUtils.d.ts.map +1 -1
- package/dist/utils/cacheUtils.d.ts.map +1 -1
- package/dist/utils/forgeDriver.d.ts +21 -0
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriverProxy.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +11 -0
- package/dist/utils/metadataContextUtils.d.ts.map +1 -0
- package/dist/utils/sqlUtils.d.ts.map +1 -1
- package/dist/webtriggers/applyMigrationsWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/dropTablesMigrationWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/fetchSchemaWebTrigger.d.ts.map +1 -1
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +24 -7
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/ForgeSQLCrudOperations.ts +3 -0
- package/src/core/ForgeSQLORM.ts +119 -51
- package/src/core/ForgeSQLQueryBuilder.ts +51 -17
- package/src/core/ForgeSQLSelectOperations.ts +2 -0
- package/src/lib/drizzle/extensions/additionalActions.ts +2 -0
- package/src/utils/cacheContextUtils.ts +4 -2
- package/src/utils/cacheUtils.ts +20 -8
- package/src/utils/forgeDriver.ts +22 -1
- package/src/utils/forgeDriverProxy.ts +2 -0
- package/src/utils/metadataContextUtils.ts +24 -0
- package/src/utils/sqlUtils.ts +1 -0
- package/src/webtriggers/applyMigrationsWebTrigger.ts +5 -2
- package/src/webtriggers/clearCacheSchedulerTrigger.ts +1 -0
- package/src/webtriggers/dropMigrationWebTrigger.ts +2 -0
- package/src/webtriggers/dropTablesMigrationWebTrigger.ts +2 -0
- package/src/webtriggers/fetchSchemaWebTrigger.ts +1 -0
- package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +72 -17
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
|
|
25
25
|
- ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
|
|
26
26
|
- ✅ **Raw SQL Execution**: `execute()` and `executeCacheable()` methods for direct SQL queries with local and global caching
|
|
27
|
+
- ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and Forge SQL metadata
|
|
27
28
|
- ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
|
|
28
29
|
- ✅ **Schema migration support**, allowing automatic schema evolution
|
|
29
30
|
- ✅ **Automatic entity generation** from MySQL/tidb databases
|
|
@@ -333,6 +334,16 @@ const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
|
333
334
|
300
|
|
334
335
|
);
|
|
335
336
|
|
|
337
|
+
// Raw SQL with execution metadata
|
|
338
|
+
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
339
|
+
async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
|
|
340
|
+
(totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
|
|
341
|
+
console.log(`DB execution time: ${totalDbExecutionTime}ms`);
|
|
342
|
+
console.log(`Response size: ${totalResponseSize} bytes`);
|
|
343
|
+
console.log('Forge metadata:', forgeMetadata);
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
|
|
336
347
|
// Common Table Expressions (CTEs)
|
|
337
348
|
const userStats = await forgeSQL
|
|
338
349
|
.with(
|
|
@@ -406,6 +417,16 @@ const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
|
406
417
|
[true],
|
|
407
418
|
300
|
|
408
419
|
);
|
|
420
|
+
|
|
421
|
+
// Raw SQL with execution metadata
|
|
422
|
+
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
423
|
+
async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
|
|
424
|
+
(totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
|
|
425
|
+
console.log(`DB execution time: ${totalDbExecutionTime}ms`);
|
|
426
|
+
console.log(`Response size: ${totalResponseSize} bytes`);
|
|
427
|
+
console.log('Forge metadata:', forgeMetadata);
|
|
428
|
+
}
|
|
429
|
+
);
|
|
409
430
|
```
|
|
410
431
|
|
|
411
432
|
## Setting Up Caching with @forge/kvs (Optional)
|
|
@@ -777,6 +798,7 @@ const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(
|
|
|
777
798
|
| `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
778
799
|
| `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
|
|
779
800
|
| `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
|
|
801
|
+
| `executeWithMetadata()` | Raw SQL queries with execution metrics capture | ❌ No | Local Cache |
|
|
780
802
|
| `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
|
|
781
803
|
where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
|
|
782
804
|
|
|
@@ -1163,6 +1185,17 @@ const users = await forgeSQL
|
|
|
1163
1185
|
const users = await forgeSQL
|
|
1164
1186
|
.executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300);
|
|
1165
1187
|
|
|
1188
|
+
// Using executeWithMetadata() for capturing execution metrics
|
|
1189
|
+
const usersWithMetadata = await forgeSQL
|
|
1190
|
+
.executeWithMetadata(
|
|
1191
|
+
async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
|
|
1192
|
+
(totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
|
|
1193
|
+
console.log(`DB execution time: ${totalDbExecutionTime}ms`);
|
|
1194
|
+
console.log(`Response size: ${totalResponseSize} bytes`);
|
|
1195
|
+
console.log('Forge metadata:', forgeMetadata);
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1166
1199
|
// Using execute() with complex queries
|
|
1167
1200
|
const userStats = await forgeSQL
|
|
1168
1201
|
.execute(`
|
|
@@ -1478,6 +1511,16 @@ await forgeSQL.executeWithLocalContext(async () => {
|
|
|
1478
1511
|
[true]
|
|
1479
1512
|
);
|
|
1480
1513
|
|
|
1514
|
+
// Raw SQL with execution metadata and local caching
|
|
1515
|
+
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
1516
|
+
async () => await forgeSQL.execute("SELECT id, name FROM users WHERE active = ?", [true]),
|
|
1517
|
+
(totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
|
|
1518
|
+
console.log(`DB execution time: ${totalDbExecutionTime}ms`);
|
|
1519
|
+
console.log(`Response size: ${totalResponseSize} bytes`);
|
|
1520
|
+
console.log('Forge metadata:', forgeMetadata);
|
|
1521
|
+
}
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1481
1524
|
// Insert operation - evicts local cache for users table
|
|
1482
1525
|
await forgeSQL.insert(users).values({ name: 'New User', active: true });
|
|
1483
1526
|
|
|
@@ -1646,6 +1689,16 @@ const userStats = await forgeSQL
|
|
|
1646
1689
|
})
|
|
1647
1690
|
.from(sql`activeUsers au`)
|
|
1648
1691
|
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
1692
|
+
|
|
1693
|
+
// Using executeWithMetadata() for capturing execution metrics with caching
|
|
1694
|
+
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
1695
|
+
async () => await forgeSQL.executeCacheable("SELECT * FROM users WHERE active = ?", [true], 300),
|
|
1696
|
+
(totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
|
|
1697
|
+
console.log(`DB execution time: ${totalDbExecutionTime}ms`);
|
|
1698
|
+
console.log(`Response size: ${totalResponseSize} bytes`);
|
|
1699
|
+
console.log('Forge metadata:', forgeMetadata);
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1649
1702
|
```
|
|
1650
1703
|
|
|
1651
1704
|
### Manual Cache Management
|
|
@@ -1721,6 +1774,7 @@ The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
|
|
|
1721
1774
|
| Option | Type | Description |
|
|
1722
1775
|
| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1723
1776
|
| `logRawSqlQuery` | `boolean` | Enables logging of raw SQL queries in the Atlassian Forge Developer Console. Useful for debugging and monitoring. Defaults to `false`. |
|
|
1777
|
+
| `logCache` | `boolean` | Enables logging of cache operations (hits, misses, evictions) in the Atlassian Forge Developer Console. Useful for debugging caching issues. Defaults to `false`. |
|
|
1724
1778
|
| `disableOptimisticLocking` | `boolean` | Disables optimistic locking. When set to `true`, no additional condition (e.g., a version check) is added during record updates, which can improve performance. However, this may lead to conflicts when multiple transactions attempt to update the same record concurrently. |
|
|
1725
1779
|
| `additionalMetadata` | `object` | Allows adding custom metadata to all entities. This is useful for tracking common fields across all tables (e.g., `createdAt`, `updatedAt`, `createdBy`, etc.). The metadata will be automatically added to all generated entities. |
|
|
1726
1780
|
| `cacheEntityName` | `string` | KVS Custom entity name for cache storage. Must match the `name` in your `manifest.yml` storage entities configuration. Required for caching functionality. Defaults to `"cache"`. |
|
|
@@ -2076,19 +2130,38 @@ export const memoryUsageTrigger = () =>
|
|
|
2076
2130
|
|
|
2077
2131
|
// Conservative memory monitoring: 4MB warning (well below 16MB limit)
|
|
2078
2132
|
export const conservativeMemoryTrigger = () =>
|
|
2079
|
-
topSlowestStatementLastHourTrigger(forgeSQL,
|
|
2133
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { memoryThresholdBytes: 4 * 1024 * 1024 });
|
|
2080
2134
|
|
|
2081
2135
|
// Aggressive memory monitoring: 12MB warning (75% of 16MB limit)
|
|
2082
2136
|
export const aggressiveMemoryTrigger = () =>
|
|
2083
|
-
topSlowestStatementLastHourTrigger(forgeSQL,
|
|
2137
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { memoryThresholdBytes: 12 * 1024 * 1024 });
|
|
2084
2138
|
|
|
2085
2139
|
// Memory-only monitoring: Only trigger on memory usage (latency effectively disabled)
|
|
2086
2140
|
export const memoryOnlyTrigger = () =>
|
|
2087
|
-
topSlowestStatementLastHourTrigger(forgeSQL, 10000, 4 * 1024 * 1024);
|
|
2141
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { warnThresholdMs: 10000, memoryThresholdBytes: 4 * 1024 * 1024 });
|
|
2088
2142
|
|
|
2089
2143
|
// Latency-only monitoring: Only trigger on slow queries (memory effectively disabled)
|
|
2090
2144
|
export const latencyOnlyTrigger = () =>
|
|
2091
|
-
topSlowestStatementLastHourTrigger(forgeSQL, 500, 16 * 1024 * 1024);
|
|
2145
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { warnThresholdMs: 500, memoryThresholdBytes: 16 * 1024 * 1024 });
|
|
2146
|
+
|
|
2147
|
+
// With execution plan in logs
|
|
2148
|
+
export const withPlanTrigger = () =>
|
|
2149
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true });
|
|
2150
|
+
|
|
2151
|
+
// With cache logging enabled
|
|
2152
|
+
export const withCacheLoggingTrigger = () =>
|
|
2153
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { logCache: true });
|
|
2154
|
+
|
|
2155
|
+
// With both execution plan and cache logging
|
|
2156
|
+
export const withFullLoggingTrigger = () =>
|
|
2157
|
+
topSlowestStatementLastHourTrigger(forgeSQL, { showPlan: true, logCache: true });
|
|
2158
|
+
|
|
2159
|
+
// With custom ORM options for debugging
|
|
2160
|
+
const forgeSQL = new ForgeSQL({
|
|
2161
|
+
logRawSqlQuery: true,
|
|
2162
|
+
logCache: true,
|
|
2163
|
+
cacheEntityName: "cache"
|
|
2164
|
+
});
|
|
2092
2165
|
|
|
2093
2166
|
|
|
2094
2167
|
#### 3. Configure in manifest.yml
|
|
@@ -2196,7 +2269,8 @@ When used as a **web trigger**, the system:
|
|
|
2196
2269
|
|-----------|------|---------|-------------|
|
|
2197
2270
|
| `warnThresholdMs` | `number` | `300` | Latency threshold in milliseconds (secondary) |
|
|
2198
2271
|
| `memoryThresholdBytes` | `number` | `8 * 1024 * 1024` | **Memory usage threshold in bytes (primary focus)** |
|
|
2199
|
-
| `
|
|
2272
|
+
| `showPlan` | `boolean` | `false` | Whether to include execution plan in logs |
|
|
2273
|
+
| `logCache` | `boolean` | `false` | Whether to log cache operations |
|
|
2200
2274
|
|
|
2201
2275
|
**⚠️ Important: OR Logic**
|
|
2202
2276
|
The monitoring uses **OR logic** - if **either** threshold is exceeded, the query will be logged/returned:
|
|
@@ -2208,6 +2282,8 @@ The monitoring uses **OR logic** - if **either** threshold is exceeded, the quer
|
|
|
2208
2282
|
- **Memory-only monitoring**: Set `warnThresholdMs` to a very high value (e.g., 10000ms) to trigger only on memory usage
|
|
2209
2283
|
- **Latency-only monitoring**: Set `memoryThresholdBytes` to 16MB (16 * 1024 * 1024) to trigger only on latency
|
|
2210
2284
|
- **Combined monitoring**: Use both thresholds for comprehensive monitoring
|
|
2285
|
+
- **Execution plan analysis**: Set `showPlan: true` to include detailed execution plans in logs (useful for debugging)
|
|
2286
|
+
- **Cache debugging**: Set `logCache: true` to log cache operations and debug caching issues
|
|
2211
2287
|
|
|
2212
2288
|
**Memory Threshold Guidelines:**
|
|
2213
2289
|
- **Conservative**: 4MB (25% of 16MB limit)
|
|
@@ -2312,6 +2388,16 @@ const cachedRawUsers = await forgeSQL.executeCacheable(
|
|
|
2312
2388
|
300
|
|
2313
2389
|
);
|
|
2314
2390
|
|
|
2391
|
+
// ✅ Raw SQL execution with metadata capture
|
|
2392
|
+
const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
2393
|
+
async () => await forgeSQL.execute("SELECT * FROM users WHERE active = ?", [true]),
|
|
2394
|
+
(totalDbExecutionTime, totalResponseSize, forgeMetadata) => {
|
|
2395
|
+
console.log(`DB execution time: ${totalDbExecutionTime}ms`);
|
|
2396
|
+
console.log(`Response size: ${totalResponseSize} bytes`);
|
|
2397
|
+
console.log('Forge metadata:', forgeMetadata);
|
|
2398
|
+
}
|
|
2399
|
+
);
|
|
2400
|
+
|
|
2315
2401
|
// ✅ Common Table Expressions (CTEs)
|
|
2316
2402
|
const userStats = await forgeSQL
|
|
2317
2403
|
.with(
|
package/dist/ForgeSQLORM.js
CHANGED
|
@@ -410,7 +410,7 @@ async function clearCursorCache(tables, cursor, options) {
|
|
|
410
410
|
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
411
411
|
}
|
|
412
412
|
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
413
|
-
if (options.
|
|
413
|
+
if (options.logCache) {
|
|
414
414
|
console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
415
415
|
}
|
|
416
416
|
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
@@ -431,7 +431,7 @@ async function clearExpirationCursorCache(cursor, options) {
|
|
|
431
431
|
entityQueryBuilder = entityQueryBuilder.cursor(cursor);
|
|
432
432
|
}
|
|
433
433
|
const listResult = await entityQueryBuilder.limit(100).getMany();
|
|
434
|
-
if (options.
|
|
434
|
+
if (options.logCache) {
|
|
435
435
|
console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
|
|
436
436
|
}
|
|
437
437
|
await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
|
|
@@ -480,7 +480,7 @@ async function clearTablesCache(tables, options) {
|
|
|
480
480
|
"clearing cache"
|
|
481
481
|
);
|
|
482
482
|
} finally {
|
|
483
|
-
if (options.
|
|
483
|
+
if (options.logCache) {
|
|
484
484
|
const duration = luxon.DateTime.now().toSeconds() - startTime.toSeconds();
|
|
485
485
|
console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
|
|
486
486
|
}
|
|
@@ -499,7 +499,7 @@ async function clearExpiredCache(options) {
|
|
|
499
499
|
);
|
|
500
500
|
} finally {
|
|
501
501
|
const duration = luxon.DateTime.now().toSeconds() - startTime.toSeconds();
|
|
502
|
-
if (options?.
|
|
502
|
+
if (options?.logCache) {
|
|
503
503
|
console.debug(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
|
|
504
504
|
}
|
|
505
505
|
}
|
|
@@ -514,7 +514,7 @@ async function getFromCache(query, options) {
|
|
|
514
514
|
const sqlQuery = query.toSQL();
|
|
515
515
|
const key = hashKey(sqlQuery);
|
|
516
516
|
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
517
|
-
if (options.
|
|
517
|
+
if (options.logCache) {
|
|
518
518
|
console.warn(`Context contains value to clear. Skip getting from cache`);
|
|
519
519
|
}
|
|
520
520
|
return void 0;
|
|
@@ -522,7 +522,7 @@ async function getFromCache(query, options) {
|
|
|
522
522
|
try {
|
|
523
523
|
const cacheResult = await kvs.kvs.entity(options.cacheEntityName).get(key);
|
|
524
524
|
if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
|
|
525
|
-
if (options.
|
|
525
|
+
if (options.logCache) {
|
|
526
526
|
console.warn(`Get value from cache, cacheKey: ${key}`);
|
|
527
527
|
}
|
|
528
528
|
const results = cacheResult[dataName];
|
|
@@ -543,7 +543,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
|
|
|
543
543
|
const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
|
|
544
544
|
const sqlQuery = query.toSQL();
|
|
545
545
|
if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
|
|
546
|
-
if (options.
|
|
546
|
+
if (options.logCache) {
|
|
547
547
|
console.warn(`Context contains value to clear. Skip setting from cache`);
|
|
548
548
|
}
|
|
549
549
|
return;
|
|
@@ -558,7 +558,7 @@ async function setCacheResult(query, options, results, cacheTtl) {
|
|
|
558
558
|
},
|
|
559
559
|
{ entityName: options.cacheEntityName }
|
|
560
560
|
).execute();
|
|
561
|
-
if (options.
|
|
561
|
+
if (options.logCache) {
|
|
562
562
|
console.warn(`Store value to cache, cacheKey: ${key}`);
|
|
563
563
|
}
|
|
564
564
|
} catch (error) {
|
|
@@ -586,7 +586,7 @@ async function saveQueryLocalCacheQuery(query, rows, options) {
|
|
|
586
586
|
sql: sql2.toSQL().sql.toLowerCase(),
|
|
587
587
|
data: rows
|
|
588
588
|
};
|
|
589
|
-
if (options.
|
|
589
|
+
if (options.logCache) {
|
|
590
590
|
const q = sql2.toSQL();
|
|
591
591
|
console.debug(
|
|
592
592
|
`[forge-sql-orm][local-cache][SAVE] Stored result in cache. sql="${q.sql}", params=${JSON.stringify(q.params)}`
|
|
@@ -603,7 +603,7 @@ async function getQueryLocalCacheQuery(query, options) {
|
|
|
603
603
|
const sql2 = query;
|
|
604
604
|
const key = hashKey(sql2.toSQL());
|
|
605
605
|
if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
|
|
606
|
-
if (options.
|
|
606
|
+
if (options.logCache) {
|
|
607
607
|
const q = sql2.toSQL();
|
|
608
608
|
console.debug(
|
|
609
609
|
`[forge-sql-orm][local-cache][HIT] Returned cached result. sql="${q.sql}", params=${JSON.stringify(q.params)}`
|
|
@@ -1034,6 +1034,18 @@ class ForgeSQLSelectOperations {
|
|
|
1034
1034
|
return updateQueryResponseResults.rows;
|
|
1035
1035
|
}
|
|
1036
1036
|
}
|
|
1037
|
+
const metadataQueryContext = new node_async_hooks.AsyncLocalStorage();
|
|
1038
|
+
async function saveMetaDataInContextContext(metadata) {
|
|
1039
|
+
const context = metadataQueryContext.getStore();
|
|
1040
|
+
if (context && metadata) {
|
|
1041
|
+
context.totalResponseSize += metadata.responseSize;
|
|
1042
|
+
context.totalDbExecutionTime += metadata.dbExecutionTime;
|
|
1043
|
+
context.lastMetadata = metadata;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
async function getLastestMetadata() {
|
|
1047
|
+
return metadataQueryContext.getStore();
|
|
1048
|
+
}
|
|
1037
1049
|
const forgeDriver = async (query, params, method) => {
|
|
1038
1050
|
if (method == "execute") {
|
|
1039
1051
|
const sqlStatement = sql$1.sql.prepare(query);
|
|
@@ -1049,6 +1061,7 @@ const forgeDriver = async (query, params, method) => {
|
|
|
1049
1061
|
await sqlStatement.bindParams(...params);
|
|
1050
1062
|
}
|
|
1051
1063
|
const result = await sqlStatement.execute();
|
|
1064
|
+
await saveMetaDataInContextContext(result.metadata);
|
|
1052
1065
|
let rows;
|
|
1053
1066
|
rows = result.rows.map((r) => Object.values(r));
|
|
1054
1067
|
return { rows };
|
|
@@ -1798,6 +1811,7 @@ class ForgeSQLORMImpl {
|
|
|
1798
1811
|
try {
|
|
1799
1812
|
const newOptions = options ?? {
|
|
1800
1813
|
logRawSqlQuery: false,
|
|
1814
|
+
logCache: false,
|
|
1801
1815
|
disableOptimisticLocking: false,
|
|
1802
1816
|
cacheWrapTable: true,
|
|
1803
1817
|
cacheTTL: 120,
|
|
@@ -1823,6 +1837,42 @@ class ForgeSQLORMImpl {
|
|
|
1823
1837
|
throw error;
|
|
1824
1838
|
}
|
|
1825
1839
|
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Executes a query and provides access to execution metadata.
|
|
1842
|
+
* This method allows you to capture detailed information about query execution
|
|
1843
|
+
* including database execution time, response size, and Forge SQL metadata.
|
|
1844
|
+
*
|
|
1845
|
+
* @template T - The return type of the query
|
|
1846
|
+
* @param query - A function that returns a Promise with the query result
|
|
1847
|
+
* @param onMetadata - Callback function that receives execution metadata
|
|
1848
|
+
* @returns Promise with the query result
|
|
1849
|
+
* @example
|
|
1850
|
+
* ```typescript
|
|
1851
|
+
* const result = await forgeSQL.executeWithMetadata(
|
|
1852
|
+
* async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
|
|
1853
|
+
* (dbTime, responseSize, metadata) => {
|
|
1854
|
+
* console.log(`DB execution time: ${dbTime}ms`);
|
|
1855
|
+
* console.log(`Response size: ${responseSize} bytes`);
|
|
1856
|
+
* console.log('Forge metadata:', metadata);
|
|
1857
|
+
* }
|
|
1858
|
+
* );
|
|
1859
|
+
* ```
|
|
1860
|
+
*/
|
|
1861
|
+
async executeWithMetadata(query, onMetadata) {
|
|
1862
|
+
return metadataQueryContext.run({
|
|
1863
|
+
totalDbExecutionTime: 0,
|
|
1864
|
+
totalResponseSize: 0
|
|
1865
|
+
}, async () => {
|
|
1866
|
+
try {
|
|
1867
|
+
return await query();
|
|
1868
|
+
} finally {
|
|
1869
|
+
const metadata = await getLastestMetadata();
|
|
1870
|
+
if (metadata && metadata.lastMetadata) {
|
|
1871
|
+
await onMetadata(metadata.totalDbExecutionTime, metadata.totalResponseSize, metadata.lastMetadata);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1826
1876
|
/**
|
|
1827
1877
|
* Executes operations within a cache context that collects cache eviction events.
|
|
1828
1878
|
* All clearCache calls within the context are collected and executed in batch at the end.
|
|
@@ -2270,6 +2320,30 @@ class ForgeSQLORM {
|
|
|
2270
2320
|
constructor(options) {
|
|
2271
2321
|
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
|
|
2272
2322
|
}
|
|
2323
|
+
/**
|
|
2324
|
+
* Executes a query and provides access to execution metadata.
|
|
2325
|
+
* This method allows you to capture detailed information about query execution
|
|
2326
|
+
* including database execution time, response size, and Forge SQL metadata.
|
|
2327
|
+
*
|
|
2328
|
+
* @template T - The return type of the query
|
|
2329
|
+
* @param query - A function that returns a Promise with the query result
|
|
2330
|
+
* @param onMetadata - Callback function that receives execution metadata
|
|
2331
|
+
* @returns Promise with the query result
|
|
2332
|
+
* @example
|
|
2333
|
+
* ```typescript
|
|
2334
|
+
* const result = await forgeSQL.executeWithMetadata(
|
|
2335
|
+
* async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
|
|
2336
|
+
* (dbTime, responseSize, metadata) => {
|
|
2337
|
+
* console.log(`DB execution time: ${dbTime}ms`);
|
|
2338
|
+
* console.log(`Response size: ${responseSize} bytes`);
|
|
2339
|
+
* console.log('Forge metadata:', metadata);
|
|
2340
|
+
* }
|
|
2341
|
+
* );
|
|
2342
|
+
* ```
|
|
2343
|
+
*/
|
|
2344
|
+
async executeWithMetadata(query, onMetadata) {
|
|
2345
|
+
return this.ormInstance.executeWithMetadata(query, onMetadata);
|
|
2346
|
+
}
|
|
2273
2347
|
selectCacheable(fields, cacheTTL) {
|
|
2274
2348
|
return this.ormInstance.selectCacheable(fields, cacheTTL);
|
|
2275
2349
|
}
|
|
@@ -3218,7 +3292,25 @@ const clearCacheSchedulerTrigger = async (options) => {
|
|
|
3218
3292
|
};
|
|
3219
3293
|
}
|
|
3220
3294
|
};
|
|
3221
|
-
const
|
|
3295
|
+
const DEFAULT_MEMORY_THRESHOLD = 8 * 1024 * 1024;
|
|
3296
|
+
const DEFAULT_TIMEOUT = 300;
|
|
3297
|
+
const topSlowestStatementLastHourTrigger = async (orm, options) => {
|
|
3298
|
+
if (!orm) {
|
|
3299
|
+
return {
|
|
3300
|
+
statusCode: 500,
|
|
3301
|
+
headers: { "Content-Type": ["application/json"] },
|
|
3302
|
+
body: JSON.stringify({
|
|
3303
|
+
success: false,
|
|
3304
|
+
message: "ORM instance is required",
|
|
3305
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3306
|
+
})
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
let newOptions = options ?? {
|
|
3310
|
+
warnThresholdMs: DEFAULT_TIMEOUT,
|
|
3311
|
+
memoryThresholdBytes: DEFAULT_MEMORY_THRESHOLD,
|
|
3312
|
+
showPlan: false
|
|
3313
|
+
};
|
|
3222
3314
|
const nsToMs = (v) => {
|
|
3223
3315
|
const n = Number(v);
|
|
3224
3316
|
return Number.isFinite(n) ? n / 1e6 : NaN;
|
|
@@ -3228,6 +3320,17 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
|
|
|
3228
3320
|
return Number.isFinite(n) ? n / (1024 * 1024) : NaN;
|
|
3229
3321
|
};
|
|
3230
3322
|
const jsonSafeStringify = (value) => JSON.stringify(value, (_k, v) => typeof v === "bigint" ? v.toString() : v);
|
|
3323
|
+
function sanitizeSQL(sql2, maxLen = 1e3) {
|
|
3324
|
+
let s = sql2;
|
|
3325
|
+
s = s.replace(/--[^\n\r]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3326
|
+
s = s.replace(/'(?:\\'|[^'])*'/g, "?");
|
|
3327
|
+
s = s.replace(/\b-?\d+(?:\.\d+)?\b/g, "?");
|
|
3328
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
3329
|
+
if (s.length > maxLen) {
|
|
3330
|
+
s = s.slice(0, maxLen) + " …[truncated]";
|
|
3331
|
+
}
|
|
3332
|
+
return s;
|
|
3333
|
+
}
|
|
3231
3334
|
const TOP_N = 1;
|
|
3232
3335
|
try {
|
|
3233
3336
|
const summaryHistory = clusterStatementsSummaryHistory;
|
|
@@ -3264,7 +3367,8 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
|
|
|
3264
3367
|
const qHistory = orm.getDrizzleQueryBuilder().select(selectShape(summaryHistory)).from(summaryHistory).where(lastHourFilterHistory);
|
|
3265
3368
|
const qSummary = orm.getDrizzleQueryBuilder().select(selectShape(summary)).from(summary).where(lastHourFilterSummary);
|
|
3266
3369
|
const combined = mysqlCore.unionAll(qHistory, qSummary).as("combined");
|
|
3267
|
-
const thresholdNs = Math.floor(warnThresholdMs * 1e6);
|
|
3370
|
+
const thresholdNs = Math.floor((newOptions.warnThresholdMs ?? DEFAULT_TIMEOUT) * 1e6);
|
|
3371
|
+
const memoryThresholdBytes = newOptions.memoryThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD;
|
|
3268
3372
|
const grouped = orm.getDrizzleQueryBuilder().select({
|
|
3269
3373
|
digest: combined.digest,
|
|
3270
3374
|
stmtType: combined.stmtType,
|
|
@@ -3335,8 +3439,8 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
|
|
|
3335
3439
|
lastSeen: r.lastSeen,
|
|
3336
3440
|
planInCache: r.planInCache,
|
|
3337
3441
|
planCacheHits: r.planCacheHits,
|
|
3338
|
-
digestText: r.digestText,
|
|
3339
|
-
plan: r.plan
|
|
3442
|
+
digestText: sanitizeSQL(r.digestText),
|
|
3443
|
+
plan: newOptions.showPlan ? r.plan : void 0
|
|
3340
3444
|
}));
|
|
3341
3445
|
for (const f of formatted) {
|
|
3342
3446
|
console.warn(
|
|
@@ -3344,7 +3448,7 @@ const topSlowestStatementLastHourTrigger = async (orm, warnThresholdMs = 300, me
|
|
|
3344
3448
|
digest=${f.digest}
|
|
3345
3449
|
sql=${(f.digestText || "").slice(0, 300)}${f.digestText && f.digestText.length > 300 ? "…" : ""}`
|
|
3346
3450
|
);
|
|
3347
|
-
if (f.plan) {
|
|
3451
|
+
if (newOptions.showPlan && f.plan) {
|
|
3348
3452
|
console.warn(` full plan:
|
|
3349
3453
|
${f.plan}`);
|
|
3350
3454
|
}
|
|
@@ -3357,8 +3461,9 @@ ${f.plan}`);
|
|
|
3357
3461
|
success: true,
|
|
3358
3462
|
window: "last_1h",
|
|
3359
3463
|
top: TOP_N,
|
|
3360
|
-
warnThresholdMs,
|
|
3361
|
-
memoryThresholdBytes,
|
|
3464
|
+
warnThresholdMs: newOptions.warnThresholdMs,
|
|
3465
|
+
memoryThresholdBytes: newOptions.memoryThresholdBytes,
|
|
3466
|
+
showPlan: newOptions.showPlan,
|
|
3362
3467
|
rows: formatted,
|
|
3363
3468
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3364
3469
|
})
|