forge-sql-orm 2.1.13 → 2.1.15
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 +550 -21
- package/dist/core/ForgeSQLORM.d.ts +45 -8
- package/dist/core/ForgeSQLORM.d.ts.map +1 -1
- package/dist/core/ForgeSQLORM.js +134 -15
- package/dist/core/ForgeSQLORM.js.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.d.ts +192 -5
- package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
- package/dist/core/ForgeSQLQueryBuilder.js.map +1 -1
- package/dist/core/Rovo.d.ts +116 -0
- package/dist/core/Rovo.d.ts.map +1 -0
- package/dist/core/Rovo.js +647 -0
- package/dist/core/Rovo.js.map +1 -0
- package/dist/utils/forgeDriver.d.ts +3 -2
- package/dist/utils/forgeDriver.d.ts.map +1 -1
- package/dist/utils/forgeDriver.js +20 -16
- package/dist/utils/forgeDriver.js.map +1 -1
- package/dist/utils/metadataContextUtils.d.ts +27 -1
- package/dist/utils/metadataContextUtils.d.ts.map +1 -1
- package/dist/utils/metadataContextUtils.js +215 -12
- package/dist/utils/metadataContextUtils.js.map +1 -1
- package/dist/webtriggers/index.d.ts +1 -0
- package/dist/webtriggers/index.d.ts.map +1 -1
- package/dist/webtriggers/index.js +1 -0
- package/dist/webtriggers/index.js.map +1 -1
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts +60 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.d.ts.map +1 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.js +55 -0
- package/dist/webtriggers/topSlowestStatementLastHourTrigger.js.map +1 -0
- package/package.json +13 -11
- package/src/core/ForgeSQLORM.ts +142 -14
- package/src/core/ForgeSQLQueryBuilder.ts +213 -4
- package/src/core/Rovo.ts +765 -0
- package/src/utils/forgeDriver.ts +34 -19
- package/src/utils/metadataContextUtils.ts +267 -12
- package/src/webtriggers/index.ts +1 -0
- package/src/webtriggers/topSlowestStatementLastHourTrigger.ts +69 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
[](https://github.com/vzakharchenko/forge-sql-orm/actions/workflows/node.js.yml)
|
|
11
11
|
[](https://coveralls.io/github/vzakharchenko/forge-sql-orm?branch=master)
|
|
12
12
|
[](https://deepscan.io/dashboard#view=project&tid=26652&pid=29272&bid=940614)
|
|
13
|
+
[](https://snyk.io/test/github/vzakharchenko/forge-sql-orm)
|
|
13
14
|
|
|
14
15
|
**Forge-SQL-ORM** is an ORM designed for working with [@forge/sql](https://developer.atlassian.com/platform/forge/storage-reference/sql-tutorial/) in **Atlassian Forge**. It is built on top of [Drizzle ORM](https://orm.drizzle.team) and provides advanced capabilities for working with relational databases inside Forge.
|
|
15
16
|
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
- ✅ **Type-Safe Query Building**: Write SQL queries with full TypeScript support
|
|
23
24
|
- ✅ **Supports complex SQL queries** with joins and filtering using Drizzle ORM
|
|
24
25
|
- ✅ **Advanced Query Methods**: `selectFrom()`, `selectDistinctFrom()`, `selectCacheableFrom()`, `selectDistinctCacheableFrom()` for all-column queries with field aliasing
|
|
25
|
-
- ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and query analysis capabilities with performance monitoring
|
|
26
|
+
- ✅ **Query Execution with Metadata**: `executeWithMetadata()` method for capturing detailed execution metrics including database execution time, response size, and query analysis capabilities with performance monitoring. Supports two modes for query plan printing: TopSlowest mode (default) and SummaryTable mode
|
|
26
27
|
- ✅ **Raw SQL Execution**: `execute()`, `executeCacheable()`, `executeDDL()`, and `executeDDLActions()` methods for direct SQL queries with local and global caching
|
|
27
28
|
- ✅ **Common Table Expressions (CTEs)**: `with()` method for complex queries with subqueries
|
|
28
29
|
- ✅ **Schema migration support**, allowing automatic schema evolution
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
- ✅ **Ready-to-use Migration Triggers** Built-in web triggers for applying migrations, dropping tables (development-only), and fetching schema (development-only) with proper error handling and security controls
|
|
34
35
|
- ✅ **Optimistic Locking** Ensures data consistency by preventing conflicts when multiple users update the same record
|
|
35
36
|
- ✅ **Query Plan Analysis**: Detailed execution plan analysis and optimization insights
|
|
37
|
+
- ✅ **Rovo Integration** Secure pattern for natural-language analytics with comprehensive security validations, Row-Level Security (RLS) support, and dynamic SQL query execution
|
|
36
38
|
|
|
37
39
|
## Table of Contents
|
|
38
40
|
|
|
@@ -68,6 +70,7 @@
|
|
|
68
70
|
### 🔒 Advanced Features
|
|
69
71
|
|
|
70
72
|
- [Optimistic Locking](#optimistic-locking)
|
|
73
|
+
- [Rovo Integration](#rovo-integration) - Secure pattern for natural-language analytics with dynamic SQL queries
|
|
71
74
|
- [Query Analysis and Performance Optimization](#query-analysis-and-performance-optimization)
|
|
72
75
|
- [Automatic Error Analysis](#automatic-error-analysis) - Automatic timeout and OOM error detection with execution plans
|
|
73
76
|
- [Slow Query Monitoring](#slow-query-monitoring) - Scheduled monitoring of slow queries with execution plans
|
|
@@ -90,6 +93,7 @@
|
|
|
90
93
|
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker)
|
|
91
94
|
- [Checklist Example](examples/forge-sql-orm-example-checklist)
|
|
92
95
|
- [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities with performance monitoring
|
|
96
|
+
- [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent implementation with secure natural-language analytics
|
|
93
97
|
|
|
94
98
|
### 📚 Reference
|
|
95
99
|
|
|
@@ -109,6 +113,7 @@
|
|
|
109
113
|
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation persistent caching
|
|
110
114
|
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory invocation caching
|
|
111
115
|
- [Optimistic Locking](#optimistic-locking) - Data consistency
|
|
116
|
+
- [Rovo Integration](#rovo-integration) - Secure natural-language analytics
|
|
112
117
|
- [Migration Tools](#web-triggers-for-migrations) - Database migrations
|
|
113
118
|
- [Query Analysis](#query-analysis-and-performance-optimization) - Performance optimization
|
|
114
119
|
|
|
@@ -119,6 +124,7 @@
|
|
|
119
124
|
- [Organization Tracker Example](examples/forge-sql-orm-example-org-tracker) - Complex relationships
|
|
120
125
|
- [Checklist Example](examples/forge-sql-orm-example-checklist) - Jira integration
|
|
121
126
|
- [Cache Example](examples/forge-sql-orm-example-cache) - Advanced caching capabilities
|
|
127
|
+
- [Rovo Integration Example](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) - Real-world Rovo AI agent with secure analytics
|
|
122
128
|
|
|
123
129
|
## Usage Approaches
|
|
124
130
|
|
|
@@ -342,6 +348,11 @@ resolver.define("fetch", async (req: Request) => {
|
|
|
342
348
|
console.debug(`[Performance Debug fetch] High DB time: ${totalDbExecutionTime} ms`);
|
|
343
349
|
}
|
|
344
350
|
},
|
|
351
|
+
{
|
|
352
|
+
// Optional: Configure query plan printing behavior
|
|
353
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
354
|
+
topQueries: 3, // Print top 3 slowest queries
|
|
355
|
+
},
|
|
345
356
|
);
|
|
346
357
|
} catch (e) {
|
|
347
358
|
const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
|
|
@@ -351,12 +362,46 @@ resolver.define("fetch", async (req: Request) => {
|
|
|
351
362
|
});
|
|
352
363
|
```
|
|
353
364
|
|
|
354
|
-
|
|
365
|
+
**Query Plan Printing Options:**
|
|
366
|
+
|
|
367
|
+
The `printQueriesWithPlan` function supports two modes:
|
|
368
|
+
|
|
369
|
+
1. **TopSlowest Mode (default)**: Prints execution plans for the slowest queries from the current resolver invocation
|
|
370
|
+
- `mode`: Set to `'TopSlowest'` (default)
|
|
371
|
+
- `topQueries`: Number of top slowest queries to analyze (default: 1)
|
|
372
|
+
|
|
373
|
+
2. **SummaryTable Mode**: Uses `CLUSTER_STATEMENTS_SUMMARY` for query analysis
|
|
374
|
+
- `mode`: Set to `'SummaryTable'`
|
|
375
|
+
- `summaryTableWindowTime`: Time window in milliseconds (default: 15000ms)
|
|
376
|
+
- Only works if queries are executed within the specified time window
|
|
377
|
+
|
|
378
|
+
### 5. Rovo Integration (Secure Analytics)
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
// Secure dynamic SQL queries for natural-language analytics
|
|
382
|
+
const rovo = forgeSQL.rovo();
|
|
383
|
+
const settings = await rovo
|
|
384
|
+
.rovoSettingBuilder(usersTable, accountId)
|
|
385
|
+
.addContextParameter(":currentUserId", accountId)
|
|
386
|
+
.useRLS()
|
|
387
|
+
.addRlsColumn(usersTable.id)
|
|
388
|
+
.addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
|
|
389
|
+
.finish()
|
|
390
|
+
.build();
|
|
391
|
+
|
|
392
|
+
const result = await rovo.dynamicIsolatedQuery(
|
|
393
|
+
"SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
|
|
394
|
+
settings,
|
|
395
|
+
);
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### 6. Next Steps
|
|
355
399
|
|
|
356
400
|
- [Full Installation Guide](#installation) - Complete setup instructions
|
|
357
401
|
- [Core Features](#core-features) - Learn about key capabilities
|
|
358
402
|
- [Global Cache System (Level 2)](#global-cache-system-level-2) - Cross-invocation caching features
|
|
359
403
|
- [Local Cache System (Level 1)](#local-cache-operations-level-1) - In-memory caching features
|
|
404
|
+
- [Rovo Integration](#rovo-integration) - Secure natural-language analytics
|
|
360
405
|
- [API Reference](#reference) - Complete API documentation
|
|
361
406
|
|
|
362
407
|
## Drizzle Usage with forge-sql-orm
|
|
@@ -426,6 +471,11 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
426
471
|
|
|
427
472
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
428
473
|
},
|
|
474
|
+
{
|
|
475
|
+
// Optional: Configure query plan printing
|
|
476
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
477
|
+
topQueries: 2, // Print top 2 slowest queries
|
|
478
|
+
},
|
|
429
479
|
);
|
|
430
480
|
|
|
431
481
|
// DDL operations for schema modifications
|
|
@@ -466,6 +516,22 @@ const userStats = await forgeSQL
|
|
|
466
516
|
})
|
|
467
517
|
.from(sql`activeUsers au`)
|
|
468
518
|
.leftJoin(sql`completedOrders co`, eq(sql`au.id`, sql`co.userId`));
|
|
519
|
+
|
|
520
|
+
// Rovo Integration for secure dynamic SQL queries
|
|
521
|
+
const rovo = forgeSQL.rovo();
|
|
522
|
+
const settings = await rovo
|
|
523
|
+
.rovoSettingBuilder(usersTable, accountId)
|
|
524
|
+
.addContextParameter(":currentUserId", accountId)
|
|
525
|
+
.useRLS()
|
|
526
|
+
.addRlsColumn(usersTable.id)
|
|
527
|
+
.addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
|
|
528
|
+
.finish()
|
|
529
|
+
.build();
|
|
530
|
+
|
|
531
|
+
const rovoResult = await rovo.dynamicIsolatedQuery(
|
|
532
|
+
"SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
|
|
533
|
+
settings,
|
|
534
|
+
);
|
|
469
535
|
```
|
|
470
536
|
|
|
471
537
|
This approach gives you direct access to all Drizzle ORM features while still using the @forge/sql backend with enhanced caching and versioning capabilities.
|
|
@@ -547,7 +613,12 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
547
613
|
}
|
|
548
614
|
|
|
549
615
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
550
|
-
}
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
// Optional: Configure query plan printing
|
|
619
|
+
mode: 'TopSlowest', // Print top slowest queries (default)
|
|
620
|
+
topQueries: 1, // Print top slowest query
|
|
621
|
+
},
|
|
551
622
|
);
|
|
552
623
|
```
|
|
553
624
|
|
|
@@ -951,23 +1022,23 @@ const optimizedData = await forgeSQL.executeWithLocalCacheContextAndReturnValue(
|
|
|
951
1022
|
|
|
952
1023
|
### When to Use Each Approach
|
|
953
1024
|
|
|
954
|
-
| Method | Use Case
|
|
955
|
-
| ---------------------------------------------------------------------- |
|
|
956
|
-
| `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations
|
|
957
|
-
| `insertAndEvictCache()` | Simple inserts without conflicts
|
|
958
|
-
| `updateAndEvictCache()` | Simple updates without conflicts
|
|
959
|
-
| `deleteAndEvictCache()` | Simple deletes without conflicts
|
|
960
|
-
| `insert/update/delete` | Basic Drizzle operations
|
|
961
|
-
| `selectFrom()` | All-column queries with field aliasing
|
|
962
|
-
| `selectDistinctFrom()` | Distinct all-column queries with field aliasing
|
|
963
|
-
| `selectCacheableFrom()` | All-column queries with field aliasing and caching
|
|
964
|
-
| `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching
|
|
965
|
-
| `execute()` | Raw SQL queries with local caching
|
|
966
|
-
| `executeCacheable()` | Raw SQL queries with local and global caching
|
|
967
|
-
| `executeWithMetadata()` |
|
|
968
|
-
| `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.)
|
|
969
|
-
| `executeDDLActions()` | Execute regular SQL queries in DDL operation context
|
|
970
|
-
| `with()` | Common Table Expressions (CTEs)
|
|
1025
|
+
| Method | Use Case | Versioning | Cache Management |
|
|
1026
|
+
| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------- | -------------------- |
|
|
1027
|
+
| `insertWithCacheContext/insertWithCacheContext/updateWithCacheContext` | Basic Drizzle operations | ❌ No | Cache Context |
|
|
1028
|
+
| `insertAndEvictCache()` | Simple inserts without conflicts | ❌ No | ✅ Yes |
|
|
1029
|
+
| `updateAndEvictCache()` | Simple updates without conflicts | ❌ No | ✅ Yes |
|
|
1030
|
+
| `deleteAndEvictCache()` | Simple deletes without conflicts | ❌ No | ✅ Yes |
|
|
1031
|
+
| `insert/update/delete` | Basic Drizzle operations | ❌ No | ❌ No |
|
|
1032
|
+
| `selectFrom()` | All-column queries with field aliasing | ❌ No | Local Cache |
|
|
1033
|
+
| `selectDistinctFrom()` | Distinct all-column queries with field aliasing | ❌ No | Local Cache |
|
|
1034
|
+
| `selectCacheableFrom()` | All-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
1035
|
+
| `selectDistinctCacheableFrom()` | Distinct all-column queries with field aliasing and caching | ❌ No | Local + Global Cache |
|
|
1036
|
+
| `execute()` | Raw SQL queries with local caching | ❌ No | Local Cache |
|
|
1037
|
+
| `executeCacheable()` | Raw SQL queries with local and global caching | ❌ No | Local + Global Cache |
|
|
1038
|
+
| `executeWithMetadata()` | Resolver-level profiling with execution metrics and configurable query plan printing (TopSlowest or SummaryTable mode) | ❌ No | Local Cache |
|
|
1039
|
+
| `executeDDL()` | DDL operations (CREATE, ALTER, DROP, etc.) | ❌ No | No Caching |
|
|
1040
|
+
| `executeDDLActions()` | Execute regular SQL queries in DDL operation context | ❌ No | No Caching |
|
|
1041
|
+
| `with()` | Common Table Expressions (CTEs) | ❌ No | Local Cache |
|
|
971
1042
|
|
|
972
1043
|
where Cache context - allows you to batch cache invalidation events and bypass cache reads for affected tables.
|
|
973
1044
|
|
|
@@ -1371,7 +1442,12 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
1371
1442
|
}
|
|
1372
1443
|
|
|
1373
1444
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
1374
|
-
}
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
// Optional: Configure query plan printing
|
|
1448
|
+
mode: 'TopSlowest', // Print top slowest queries (default)
|
|
1449
|
+
topQueries: 1, // Print top slowest query
|
|
1450
|
+
},
|
|
1375
1451
|
);
|
|
1376
1452
|
|
|
1377
1453
|
// Using executeDDL() for DDL operations (CREATE, ALTER, DROP, etc.)
|
|
@@ -1739,6 +1815,11 @@ await forgeSQL.executeWithLocalContext(async () => {
|
|
|
1739
1815
|
|
|
1740
1816
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
1741
1817
|
},
|
|
1818
|
+
{
|
|
1819
|
+
// Optional: Configure query plan printing
|
|
1820
|
+
topQueries: 1, // Print top slowest query (default)
|
|
1821
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
1822
|
+
},
|
|
1742
1823
|
);
|
|
1743
1824
|
|
|
1744
1825
|
// Insert operation - evicts local cache for users table
|
|
@@ -1940,6 +2021,11 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
1940
2021
|
|
|
1941
2022
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
1942
2023
|
},
|
|
2024
|
+
{
|
|
2025
|
+
// Optional: Configure query plan printing
|
|
2026
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
2027
|
+
topQueries: 1, // Print top slowest query
|
|
2028
|
+
},
|
|
1943
2029
|
);
|
|
1944
2030
|
```
|
|
1945
2031
|
|
|
@@ -2011,6 +2097,223 @@ await forgeSQL.modifyWithVersioningAndEvictCache().updateById(
|
|
|
2011
2097
|
);
|
|
2012
2098
|
```
|
|
2013
2099
|
|
|
2100
|
+
## Rovo Integration
|
|
2101
|
+
|
|
2102
|
+
[↑ Back to Top](#table-of-contents)
|
|
2103
|
+
|
|
2104
|
+
Rovo is a secure pattern for natural-language analytics in Forge apps. It enables safe execution of dynamic SQL queries with comprehensive security validations, making it ideal for AI-powered analytics features where users can query data using natural language.
|
|
2105
|
+
|
|
2106
|
+
**📖 Real-World Example**: See [Forge-Secure-Notes-for-Jira](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) for a complete implementation of Rovo AI agent with secure natural-language analytics.
|
|
2107
|
+
|
|
2108
|
+
### Key Features
|
|
2109
|
+
|
|
2110
|
+
- **Security-First Design**: Multiple layers of security validations to prevent SQL injection and unauthorized data access
|
|
2111
|
+
- **Single Table Isolation**: Queries are restricted to a single table to prevent cross-table data access
|
|
2112
|
+
- **Row-Level Security (RLS)**: Built-in support for data isolation based on user context
|
|
2113
|
+
- **Comprehensive Validation**: Blocks JOINs, subqueries, window functions, and other potentially unsafe operations
|
|
2114
|
+
- **Post-Execution Validation**: Verifies query results to ensure security fields are present and come from the correct table
|
|
2115
|
+
- **Type-Safe Configuration**: Uses Drizzle ORM table objects for type-safe column references
|
|
2116
|
+
|
|
2117
|
+
### Security Validations
|
|
2118
|
+
|
|
2119
|
+
Rovo performs multiple security checks before and after query execution:
|
|
2120
|
+
|
|
2121
|
+
1. **Query Type Validation**: Only SELECT queries are allowed
|
|
2122
|
+
2. **Table Restriction**: Queries must target only the specified table
|
|
2123
|
+
3. **JOIN Detection**: JOINs are blocked using EXPLAIN analysis
|
|
2124
|
+
4. **Subquery Detection**: Scalar subqueries in SELECT columns are blocked
|
|
2125
|
+
5. **Window Function Detection**: Window functions are blocked for security
|
|
2126
|
+
6. **Execution Plan Validation**: Verifies that only the expected table is accessed
|
|
2127
|
+
7. **RLS Field Validation**: Ensures required security fields are present in results
|
|
2128
|
+
8. **Post-Execution Validation**: Verifies all fields come from the correct table
|
|
2129
|
+
|
|
2130
|
+
### Basic Usage
|
|
2131
|
+
|
|
2132
|
+
```typescript
|
|
2133
|
+
import ForgeSQL from "forge-sql-orm";
|
|
2134
|
+
|
|
2135
|
+
const forgeSQL = new ForgeSQL();
|
|
2136
|
+
|
|
2137
|
+
// Get Rovo instance
|
|
2138
|
+
const rovo = forgeSQL.rovo();
|
|
2139
|
+
|
|
2140
|
+
// Create settings builder using Drizzle table object
|
|
2141
|
+
const settings = await rovo
|
|
2142
|
+
.rovoSettingBuilder(usersTable, accountId)
|
|
2143
|
+
.addContextParameter(":currentUserId", accountId)
|
|
2144
|
+
.useRLS()
|
|
2145
|
+
.addRlsColumn(usersTable.id)
|
|
2146
|
+
.addRlsWherePart((alias) => `${alias}.${usersTable.id.name} = '${accountId}'`)
|
|
2147
|
+
.finish()
|
|
2148
|
+
.build();
|
|
2149
|
+
|
|
2150
|
+
// Execute dynamic SQL query
|
|
2151
|
+
const result = await rovo.dynamicIsolatedQuery(
|
|
2152
|
+
"SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
|
|
2153
|
+
settings,
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
console.log(result.rows); // Query results
|
|
2157
|
+
console.log(result.metadata); // Query metadata
|
|
2158
|
+
```
|
|
2159
|
+
|
|
2160
|
+
### Row-Level Security (RLS) Configuration
|
|
2161
|
+
|
|
2162
|
+
RLS allows you to filter data based on user context, ensuring users can only access their own data:
|
|
2163
|
+
|
|
2164
|
+
```typescript
|
|
2165
|
+
const rovo = forgeSQL.rovo();
|
|
2166
|
+
|
|
2167
|
+
// Configure RLS with conditional activation and multiple security fields
|
|
2168
|
+
const settings = await rovo
|
|
2169
|
+
.rovoSettingBuilder(securityNotesTable, accountId)
|
|
2170
|
+
.addContextParameter(":currentUserId", accountId)
|
|
2171
|
+
.addContextParameter(":currentProjectKey", projectKey)
|
|
2172
|
+
.addContextParameter(":currentIssueKey", issueKey)
|
|
2173
|
+
.useRLS()
|
|
2174
|
+
.addRlsCondition(async () => {
|
|
2175
|
+
// Conditionally enable RLS based on user role
|
|
2176
|
+
const userService = getUserService();
|
|
2177
|
+
return !(await userService.isAdmin()); // Only apply RLS for non-admin users
|
|
2178
|
+
})
|
|
2179
|
+
.addRlsColumn(securityNotesTable.createdBy) // Required field for RLS validation
|
|
2180
|
+
.addRlsColumn(securityNotesTable.targetUserId) // Additional security field
|
|
2181
|
+
.addRlsWherePart(
|
|
2182
|
+
(alias) =>
|
|
2183
|
+
`${alias}.${securityNotesTable.createdBy.name} = '${accountId}' OR ${alias}.${securityNotesTable.targetUserId.name} = '${accountId}'`,
|
|
2184
|
+
) // RLS filter with OR condition
|
|
2185
|
+
.finish()
|
|
2186
|
+
.build();
|
|
2187
|
+
|
|
2188
|
+
// The query will automatically be wrapped with RLS filtering:
|
|
2189
|
+
// SELECT * FROM (original_query) AS t WHERE (t.createdBy = 'accountId' OR t.targetUserId = 'accountId')
|
|
2190
|
+
```
|
|
2191
|
+
|
|
2192
|
+
### Context Parameters
|
|
2193
|
+
|
|
2194
|
+
You can use context parameters for query substitution. Parameters use the `:parameterName` format (colon prefix, not double braces):
|
|
2195
|
+
|
|
2196
|
+
```typescript
|
|
2197
|
+
const rovo = forgeSQL.rovo();
|
|
2198
|
+
|
|
2199
|
+
const settings = await rovo
|
|
2200
|
+
.rovoSettingBuilder(usersTable, accountId)
|
|
2201
|
+
.addContextParameter(":currentUserId", accountId)
|
|
2202
|
+
.addContextParameter(":projectKey", "PROJ-123")
|
|
2203
|
+
.addContextParameter(":status", "active")
|
|
2204
|
+
.useRLS()
|
|
2205
|
+
.addRlsColumn(usersTable.id)
|
|
2206
|
+
.addRlsWherePart((alias) => `${alias}.${usersTable.userId.name} = '${accountId}'`)
|
|
2207
|
+
.finish()
|
|
2208
|
+
.build();
|
|
2209
|
+
|
|
2210
|
+
// In the SQL query, parameters are replaced:
|
|
2211
|
+
const result = await rovo.dynamicIsolatedQuery(
|
|
2212
|
+
"SELECT * FROM users WHERE projectKey = :projectKey AND status = :status AND userId = :currentUserId",
|
|
2213
|
+
settings,
|
|
2214
|
+
);
|
|
2215
|
+
// Becomes: SELECT * FROM users WHERE projectKey = 'PROJ-123' AND status = 'active' AND userId = 'accountId'
|
|
2216
|
+
```
|
|
2217
|
+
|
|
2218
|
+
### Using Raw Table Names
|
|
2219
|
+
|
|
2220
|
+
You can use `rovoRawSettingBuilder` with raw table name string:
|
|
2221
|
+
|
|
2222
|
+
```typescript
|
|
2223
|
+
const rovo = forgeSQL.rovo();
|
|
2224
|
+
|
|
2225
|
+
// Using rovoRawSettingBuilder with raw table name
|
|
2226
|
+
const settings = await rovo
|
|
2227
|
+
.rovoRawSettingBuilder("users", accountId)
|
|
2228
|
+
.addContextParameter(":currentUserId", accountId)
|
|
2229
|
+
.useRLS()
|
|
2230
|
+
.addRlsColumnName("id")
|
|
2231
|
+
.addRlsWherePart((alias) => `${alias}.id = '${accountId}'`)
|
|
2232
|
+
.finish()
|
|
2233
|
+
.build();
|
|
2234
|
+
|
|
2235
|
+
const result = await rovo.dynamicIsolatedQuery(
|
|
2236
|
+
"SELECT id, name FROM users WHERE status = 'active' AND userId = :currentUserId",
|
|
2237
|
+
settings,
|
|
2238
|
+
);
|
|
2239
|
+
```
|
|
2240
|
+
|
|
2241
|
+
### Security Restrictions
|
|
2242
|
+
|
|
2243
|
+
Rovo blocks the following operations for security:
|
|
2244
|
+
|
|
2245
|
+
- **Data Modification**: Only SELECT queries are allowed
|
|
2246
|
+
- **JOINs**: JOIN operations are detected and blocked
|
|
2247
|
+
- **Subqueries**: Scalar subqueries in SELECT columns are blocked
|
|
2248
|
+
- **Window Functions**: Window functions (e.g., `COUNT(*) OVER(...)`) are blocked
|
|
2249
|
+
- **Multiple Tables**: Queries referencing multiple tables are blocked
|
|
2250
|
+
- **Table Aliases**: Post-execution validation ensures fields come from the correct table
|
|
2251
|
+
|
|
2252
|
+
### Error Handling
|
|
2253
|
+
|
|
2254
|
+
Rovo provides detailed error messages when security violations are detected:
|
|
2255
|
+
|
|
2256
|
+
```typescript
|
|
2257
|
+
try {
|
|
2258
|
+
const result = await rovo.dynamicIsolatedQuery(
|
|
2259
|
+
"SELECT * FROM users u JOIN orders o ON u.id = o.userId",
|
|
2260
|
+
settings,
|
|
2261
|
+
);
|
|
2262
|
+
} catch (error) {
|
|
2263
|
+
// Error: "Security violation: JOIN operations are not allowed..."
|
|
2264
|
+
console.error(error.message);
|
|
2265
|
+
}
|
|
2266
|
+
```
|
|
2267
|
+
|
|
2268
|
+
### Example: Real-World Function Implementation
|
|
2269
|
+
|
|
2270
|
+
> **💡 Full Example**: See the complete implementation in [Forge-Secure-Notes-for-Jira](https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira) repository.
|
|
2271
|
+
|
|
2272
|
+
```typescript
|
|
2273
|
+
import ForgeSQL from "forge-sql-orm";
|
|
2274
|
+
import { Result } from "@forge/sql";
|
|
2275
|
+
|
|
2276
|
+
const FORGE_SQL_ORM = new ForgeSQL();
|
|
2277
|
+
|
|
2278
|
+
export async function runSecurityNotesQuery(
|
|
2279
|
+
event: {
|
|
2280
|
+
sql: string;
|
|
2281
|
+
context: {
|
|
2282
|
+
jira: {
|
|
2283
|
+
issueKey: string;
|
|
2284
|
+
projectKey: string;
|
|
2285
|
+
};
|
|
2286
|
+
};
|
|
2287
|
+
},
|
|
2288
|
+
context: { principal: { accountId: string } },
|
|
2289
|
+
): Promise<Result<unknown>> {
|
|
2290
|
+
const rovoIntegration = FORGE_SQL_ORM.rovo();
|
|
2291
|
+
const accountId = context.principal.accountId;
|
|
2292
|
+
|
|
2293
|
+
const settings = await rovoIntegration
|
|
2294
|
+
.rovoSettingBuilder(securityNotesTable, accountId)
|
|
2295
|
+
.addContextParameter(":currentUserId", accountId)
|
|
2296
|
+
.addContextParameter(":currentProjectKey", event.context?.jira?.projectKey ?? "")
|
|
2297
|
+
.addContextParameter(":currentIssueKey", event.context?.jira?.issueKey ?? "")
|
|
2298
|
+
.useRLS()
|
|
2299
|
+
.addRlsCondition(async () => {
|
|
2300
|
+
// Conditionally disable RLS for admin users
|
|
2301
|
+
const userService = getUserService();
|
|
2302
|
+
return !(await userService.isAdmin());
|
|
2303
|
+
})
|
|
2304
|
+
.addRlsColumn(securityNotesTable.createdBy)
|
|
2305
|
+
.addRlsColumn(securityNotesTable.targetUserId)
|
|
2306
|
+
.addRlsWherePart(
|
|
2307
|
+
(alias: string) =>
|
|
2308
|
+
`${alias}.${securityNotesTable.createdBy.name} = '${accountId}' OR ${alias}.${securityNotesTable.targetUserId.name} = '${accountId}'`,
|
|
2309
|
+
)
|
|
2310
|
+
.finish()
|
|
2311
|
+
.build();
|
|
2312
|
+
|
|
2313
|
+
return await rovoIntegration.dynamicIsolatedQuery(event.sql, settings);
|
|
2314
|
+
}
|
|
2315
|
+
```
|
|
2316
|
+
|
|
2014
2317
|
## ForgeSqlOrmOptions
|
|
2015
2318
|
|
|
2016
2319
|
The `ForgeSqlOrmOptions` object allows customization of ORM behavior:
|
|
@@ -2363,6 +2666,227 @@ The error analysis mechanism:
|
|
|
2363
2666
|
|
|
2364
2667
|
> **💡 Tip**: The automatic error analysis only triggers for timeout and OOM errors. Other errors are logged normally without plan analysis.
|
|
2365
2668
|
|
|
2669
|
+
### Resolver-Level Performance Monitoring
|
|
2670
|
+
|
|
2671
|
+
The `executeWithMetadata()` method provides resolver-level profiling with configurable query plan printing. It aggregates metrics across all database operations within a resolver and supports two modes for query plan analysis.
|
|
2672
|
+
|
|
2673
|
+
#### Basic Usage
|
|
2674
|
+
|
|
2675
|
+
```typescript
|
|
2676
|
+
const result = await forgeSQL.executeWithMetadata(
|
|
2677
|
+
async () => {
|
|
2678
|
+
const users = await forgeSQL.selectFrom(usersTable);
|
|
2679
|
+
const orders = await forgeSQL
|
|
2680
|
+
.selectFrom(ordersTable)
|
|
2681
|
+
.where(eq(ordersTable.userId, usersTable.id));
|
|
2682
|
+
return { users, orders };
|
|
2683
|
+
},
|
|
2684
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2685
|
+
const threshold = 500; // ms baseline for this resolver
|
|
2686
|
+
|
|
2687
|
+
if (totalDbExecutionTime > threshold * 1.5) {
|
|
2688
|
+
console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
|
|
2689
|
+
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
2690
|
+
} else if (totalDbExecutionTime > threshold) {
|
|
2691
|
+
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
2695
|
+
},
|
|
2696
|
+
);
|
|
2697
|
+
```
|
|
2698
|
+
|
|
2699
|
+
#### Query Plan Printing Options
|
|
2700
|
+
|
|
2701
|
+
The `printQueriesWithPlan` function supports two modes, configurable via the optional `options` parameter:
|
|
2702
|
+
|
|
2703
|
+
**1. TopSlowest Mode (default)**: Prints execution plans for the slowest queries from the current resolver invocation
|
|
2704
|
+
|
|
2705
|
+
```typescript
|
|
2706
|
+
// Full configuration example
|
|
2707
|
+
const result = await forgeSQL.executeWithMetadata(
|
|
2708
|
+
async () => {
|
|
2709
|
+
const users = await forgeSQL.selectFrom(usersTable);
|
|
2710
|
+
return users;
|
|
2711
|
+
},
|
|
2712
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2713
|
+
if (totalDbExecutionTime > 1000) {
|
|
2714
|
+
await printQueriesWithPlan(); // Will print top 3 slowest queries with execution plans
|
|
2715
|
+
}
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
2719
|
+
topQueries: 3, // Number of top slowest queries to analyze (default: 1)
|
|
2720
|
+
showSlowestPlans: true, // Show execution plans (default: true)
|
|
2721
|
+
},
|
|
2722
|
+
);
|
|
2723
|
+
|
|
2724
|
+
// Minimal configuration - only specify what you need
|
|
2725
|
+
const result2 = await forgeSQL.executeWithMetadata(
|
|
2726
|
+
async () => {
|
|
2727
|
+
const users = await forgeSQL.selectFrom(usersTable);
|
|
2728
|
+
return users;
|
|
2729
|
+
},
|
|
2730
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2731
|
+
if (totalDbExecutionTime > 1000) {
|
|
2732
|
+
await printQueriesWithPlan(); // Will print top 3 slowest queries (all other options use defaults)
|
|
2733
|
+
}
|
|
2734
|
+
},
|
|
2735
|
+
{
|
|
2736
|
+
topQueries: 3, // Only specify topQueries, mode and showSlowestPlans use defaults
|
|
2737
|
+
},
|
|
2738
|
+
);
|
|
2739
|
+
|
|
2740
|
+
// Disable execution plans - only show SQL and execution time
|
|
2741
|
+
const result3 = await forgeSQL.executeWithMetadata(
|
|
2742
|
+
async () => {
|
|
2743
|
+
const users = await forgeSQL.selectFrom(usersTable);
|
|
2744
|
+
return users;
|
|
2745
|
+
},
|
|
2746
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2747
|
+
if (totalDbExecutionTime > 1000) {
|
|
2748
|
+
await printQueriesWithPlan(); // Will print SQL and time only, no execution plans
|
|
2749
|
+
}
|
|
2750
|
+
},
|
|
2751
|
+
{
|
|
2752
|
+
showSlowestPlans: false, // Disable execution plan printing
|
|
2753
|
+
},
|
|
2754
|
+
);
|
|
2755
|
+
|
|
2756
|
+
// Use all defaults - pass empty object or omit options parameter
|
|
2757
|
+
const result4 = await forgeSQL.executeWithMetadata(
|
|
2758
|
+
async () => {
|
|
2759
|
+
const users = await forgeSQL.selectFrom(usersTable);
|
|
2760
|
+
return users;
|
|
2761
|
+
},
|
|
2762
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2763
|
+
if (totalDbExecutionTime > 1000) {
|
|
2764
|
+
await printQueriesWithPlan(); // Uses all defaults: TopSlowest mode, topQueries: 1, showSlowestPlans: true
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
{}, // Empty object - all options use defaults
|
|
2768
|
+
);
|
|
2769
|
+
```
|
|
2770
|
+
|
|
2771
|
+
<|tool▁calls▁begin|><|tool▁call▁begin|>
|
|
2772
|
+
read_file
|
|
2773
|
+
|
|
2774
|
+
**2. SummaryTable Mode**: Uses `CLUSTER_STATEMENTS_SUMMARY` for query analysis
|
|
2775
|
+
|
|
2776
|
+
```typescript
|
|
2777
|
+
const result = await forgeSQL.executeWithMetadata(
|
|
2778
|
+
async () => {
|
|
2779
|
+
const users = await forgeSQL.selectFrom(usersTable);
|
|
2780
|
+
return users;
|
|
2781
|
+
},
|
|
2782
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2783
|
+
if (totalDbExecutionTime > 1000) {
|
|
2784
|
+
await printQueriesWithPlan(); // Will use CLUSTER_STATEMENTS_SUMMARY if within time window
|
|
2785
|
+
}
|
|
2786
|
+
},
|
|
2787
|
+
{
|
|
2788
|
+
mode: "SummaryTable", // Use SummaryTable mode
|
|
2789
|
+
summaryTableWindowTime: 10000, // Time window in milliseconds (default: 15000ms)
|
|
2790
|
+
},
|
|
2791
|
+
);
|
|
2792
|
+
```
|
|
2793
|
+
|
|
2794
|
+
#### Configuration Options
|
|
2795
|
+
|
|
2796
|
+
All options are **optional**. If not specified, default values are used. You can pass only the options you need to customize.
|
|
2797
|
+
|
|
2798
|
+
| Option | Type | Default | Description |
|
|
2799
|
+
| ------------------------ | -------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2800
|
+
| `mode` | `'TopSlowest' \| 'SummaryTable'` | `'TopSlowest'` | Query plan printing mode. `'TopSlowest'` prints execution plans for the slowest queries from the current resolver. `'SummaryTable'` uses `CLUSTER_STATEMENTS_SUMMARY` when within time window |
|
|
2801
|
+
| `summaryTableWindowTime` | `number` | `15000` | Time window in milliseconds for summary table queries. Only used when `mode` is `'SummaryTable'` |
|
|
2802
|
+
| `topQueries` | `number` | `1` | Number of top slowest queries to analyze when `mode` is `'TopSlowest'` |
|
|
2803
|
+
| `showSlowestPlans` | `boolean` | `true` | Whether to show execution plans for slowest queries in TopSlowest mode. If `false`, only SQL and execution time are printed |
|
|
2804
|
+
| `normalizeQuery` | `boolean` | `true` | Whether to normalize SQL queries by replacing parameter values with `?` placeholders. Set to `false` to disable normalization if it causes issues with complex queries |
|
|
2805
|
+
|
|
2806
|
+
**Examples:**
|
|
2807
|
+
|
|
2808
|
+
```typescript
|
|
2809
|
+
// Use all defaults - omit options or pass empty object
|
|
2810
|
+
await forgeSQL.executeWithMetadata(queryFn, onMetadataFn); // or { }
|
|
2811
|
+
|
|
2812
|
+
// Customize only what you need
|
|
2813
|
+
await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { topQueries: 3 });
|
|
2814
|
+
await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { mode: "SummaryTable" });
|
|
2815
|
+
await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { showSlowestPlans: false });
|
|
2816
|
+
await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, { normalizeQuery: false }); // Disable query normalization
|
|
2817
|
+
|
|
2818
|
+
// Combine multiple options
|
|
2819
|
+
await forgeSQL.executeWithMetadata(queryFn, onMetadataFn, {
|
|
2820
|
+
mode: "TopSlowest",
|
|
2821
|
+
topQueries: 5,
|
|
2822
|
+
showSlowestPlans: false,
|
|
2823
|
+
normalizeQuery: true, // Enable query normalization (default)
|
|
2824
|
+
});
|
|
2825
|
+
```
|
|
2826
|
+
|
|
2827
|
+
#### How It Works
|
|
2828
|
+
|
|
2829
|
+
1. **TopSlowest Mode** (default):
|
|
2830
|
+
- Collects all queries executed within the resolver
|
|
2831
|
+
- Sorts them by execution time (slowest first)
|
|
2832
|
+
- Prints execution plans for the top N queries (configurable via `topQueries`)
|
|
2833
|
+
- If `showSlowestPlans` is `false`, only prints SQL and execution time without plans
|
|
2834
|
+
- Works immediately after query execution
|
|
2835
|
+
|
|
2836
|
+
2. **SummaryTable Mode**:
|
|
2837
|
+
- Attempts to use `CLUSTER_STATEMENTS_SUMMARY` for query analysis
|
|
2838
|
+
- Only works if queries are executed within the specified time window (`summaryTableWindowTime`)
|
|
2839
|
+
- If the time window expires, falls back to TopSlowest mode
|
|
2840
|
+
- Provides aggregated statistics from TiDB's system tables
|
|
2841
|
+
|
|
2842
|
+
#### Example: Real-World Resolver
|
|
2843
|
+
|
|
2844
|
+
```typescript
|
|
2845
|
+
resolver.define("fetch", async (req: Request) => {
|
|
2846
|
+
try {
|
|
2847
|
+
return await forgeSQL.executeWithMetadata(
|
|
2848
|
+
async () => {
|
|
2849
|
+
const users = await forgeSQL.selectFrom(demoUsers);
|
|
2850
|
+
const orders = await forgeSQL
|
|
2851
|
+
.selectFrom(demoOrders)
|
|
2852
|
+
.where(eq(demoOrders.userId, demoUsers.id));
|
|
2853
|
+
return { users, orders };
|
|
2854
|
+
},
|
|
2855
|
+
async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
|
|
2856
|
+
const threshold = 500; // ms baseline for this resolver
|
|
2857
|
+
|
|
2858
|
+
if (totalDbExecutionTime > threshold * 1.5) {
|
|
2859
|
+
console.warn(
|
|
2860
|
+
`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`,
|
|
2861
|
+
);
|
|
2862
|
+
await printQueriesWithPlan(); // Analyze and print query execution plans
|
|
2863
|
+
} else if (totalDbExecutionTime > threshold) {
|
|
2864
|
+
console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
2869
|
+
topQueries: 2, // Print top 2 slowest queries
|
|
2870
|
+
},
|
|
2871
|
+
);
|
|
2872
|
+
} catch (e) {
|
|
2873
|
+
const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
|
|
2874
|
+
console.error(error, e);
|
|
2875
|
+
throw error;
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
```
|
|
2879
|
+
|
|
2880
|
+
#### Benefits
|
|
2881
|
+
|
|
2882
|
+
- **Resolver-Level Profiling**: Aggregates metrics across all database operations in a resolver
|
|
2883
|
+
- **Configurable Analysis**: Choose between TopSlowest mode or SummaryTable mode
|
|
2884
|
+
- **Automatic Plan Formatting**: Execution plans are formatted in a readable format
|
|
2885
|
+
- **Performance Thresholds**: Set custom thresholds for performance warnings
|
|
2886
|
+
- **Zero Configuration**: Works out of the box with sensible defaults
|
|
2887
|
+
|
|
2888
|
+
> **💡 Tip**: When multiple resolvers are running concurrently, their query data may also appear in `printQueriesWithPlan()` analysis when using SummaryTable mode, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
|
|
2889
|
+
|
|
2366
2890
|
### Slow Query Monitoring
|
|
2367
2891
|
|
|
2368
2892
|
Forge-SQL-ORM provides a scheduler trigger (`slowQuerySchedulerTrigger`) that automatically monitors and analyzes slow queries on an hourly basis. This trigger queries TiDB's slow query log system table and provides detailed performance information including SQL query text, memory usage, execution time, and execution plans.
|
|
@@ -2653,6 +3177,11 @@ const usersWithMetadata = await forgeSQL.executeWithMetadata(
|
|
|
2653
3177
|
|
|
2654
3178
|
console.log(`DB response size: ${totalResponseSize} bytes`);
|
|
2655
3179
|
},
|
|
3180
|
+
{
|
|
3181
|
+
// Optional: Configure query plan printing
|
|
3182
|
+
mode: "TopSlowest", // Print top slowest queries (default)
|
|
3183
|
+
topQueries: 1, // Print top slowest query
|
|
3184
|
+
},
|
|
2656
3185
|
);
|
|
2657
3186
|
|
|
2658
3187
|
// ✅ DDL operations for schema modifications
|