• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

vzakharchenko / forge-sql-orm / 18980807892

31 Oct 2025 05:48PM UTC coverage: 80.264% (+3.6%) from 76.616%
18980807892

push

github

vzakharchenko
added dev Observability

457 of 643 branches covered (71.07%)

Branch coverage included in aggregate %.

22 of 35 new or added lines in 6 files covered. (62.86%)

13 existing lines in 2 files now uncovered.

942 of 1100 relevant lines covered (85.64%)

16.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.51
/src/core/ForgeSQLORM.ts
1
import { ForgeSQLCrudOperations } from "./ForgeSQLCrudOperations";
2
import {
3
  VerioningModificationForgeSQL,
4
  ForgeSqlOperation,
5
  ForgeSqlOrmOptions,
6
  SchemaAnalyzeForgeSql,
7
  SchemaSqlForgeSql,
8
} from "./ForgeSQLQueryBuilder";
9
import { ForgeSQLSelectOperations } from "./ForgeSQLSelectOperations";
10
import {
11
  drizzle,
12
  MySqlRemoteDatabase,
13
  MySqlRemotePreparedQueryHKT,
14
  MySqlRemoteQueryResultHKT,
15
} from "drizzle-orm/mysql-proxy";
16
import { createForgeDriverProxy } from "../utils/forgeDriverProxy";
17
import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
18
import { MySqlSelectBuilder } from "drizzle-orm/mysql-core";
19
import {
20
  DeleteAndEvictCacheType,
21
  InsertAndEvictCacheType,
22
  patchDbWithSelectAliased,
23
  SelectAliasedCacheableType,
24
  SelectAliasedDistinctCacheableType,
25
  SelectAliasedDistinctType,
26
  SelectAliasedType,
27
  UpdateAndEvictCacheType,
28
} from "../lib/drizzle/extensions/additionalActions";
29
import { ForgeSQLAnalyseOperation } from "./ForgeSQLAnalyseOperations";
30
import { ForgeSQLCacheOperations } from "./ForgeSQLCacheOperations";
31
import { MySqlTable } from "drizzle-orm/mysql-core/table";
32
import {
33
  MySqlDeleteBase,
34
  MySqlInsertBuilder,
35
  MySqlUpdateBuilder,
36
} from "drizzle-orm/mysql-core/query-builders";
37
import { cacheApplicationContext, localCacheApplicationContext } from "../utils/cacheContextUtils";
38
import { clearTablesCache } from "../utils/cacheUtils";
39
import { SQLWrapper } from "drizzle-orm/sql/sql";
40
import { WithSubquery } from "drizzle-orm/subquery";
41
import { getLastestMetadata, metadataQueryContext } from "../utils/metadataContextUtils";
42
import { operationTypeQueryContext } from "../utils/requestTypeContextUtils";
43
import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
44

45
/**
46
 * Implementation of ForgeSQLORM that uses Drizzle ORM for query building.
47
 * This class provides a bridge between Forge SQL and Drizzle ORM, allowing
48
 * to use Drizzle's query builder while executing queries through Forge SQL.
49
 */
50
class ForgeSQLORMImpl implements ForgeSqlOperation {
51
  private static instance: ForgeSQLORMImpl | null = null;
6✔
52
  private readonly drizzle: MySqlRemoteDatabase<any> & {
53
    selectAliased: SelectAliasedType;
54
    selectAliasedDistinct: SelectAliasedDistinctType;
55
    selectAliasedCacheable: SelectAliasedCacheableType;
56
    selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
57
    insertWithCacheContext: InsertAndEvictCacheType;
58
    insertAndEvictCache: InsertAndEvictCacheType;
59
    updateAndEvictCache: UpdateAndEvictCacheType;
60
    updateWithCacheContext: UpdateAndEvictCacheType;
61
    deleteAndEvictCache: DeleteAndEvictCacheType;
62
    deleteWithCacheContext: DeleteAndEvictCacheType;
63
  };
64
  private readonly crudOperations: VerioningModificationForgeSQL;
65
  private readonly fetchOperations: SchemaSqlForgeSql;
66
  private readonly analyzeOperations: SchemaAnalyzeForgeSql;
67
  private readonly cacheOperations: ForgeSQLCacheOperations;
68
  private readonly options: ForgeSqlOrmOptions;
69

70
  /**
71
   * Private constructor to enforce singleton behavior.
72
   * @param options - Options for configuring ForgeSQL ORM behavior.
73
   */
74
  private constructor(options?: ForgeSqlOrmOptions) {
75
    try {
3✔
76
      const newOptions: ForgeSqlOrmOptions = options ?? {
3!
77
        logRawSqlQuery: false,
78
        logCache: false,
79
        disableOptimisticLocking: false,
80
        cacheWrapTable: true,
81
        cacheTTL: 120,
82
        cacheEntityQueryName: "sql",
83
        cacheEntityExpirationName: "expiration",
84
        cacheEntityDataName: "data",
85
      };
86
      this.options = newOptions;
3✔
87
      if (newOptions.logRawSqlQuery) {
3✔
88
        // eslint-disable-next-line no-console
89
        console.debug("Initializing ForgeSQLORM...");
2✔
90
      }
91
      // Initialize Drizzle instance with our custom driver
92
      const proxiedDriver = createForgeDriverProxy(
3✔
93
        this,
94
        newOptions.hints,
95
        newOptions.logRawSqlQuery,
96
      );
97
      this.drizzle = patchDbWithSelectAliased(
3✔
98
        drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
99
        newOptions,
100
      );
101
      this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
3✔
102
      this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
3✔
103
      this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
3✔
104
      this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
3✔
105
    } catch (error) {
106
      // eslint-disable-next-line no-console
107
      console.error("ForgeSQLORM initialization failed:", error);
×
108
      throw error;
×
109
    }
110
  }
111

112
  /**
113
   * Executes a query and provides access to execution metadata with performance monitoring.
114
   * This method allows you to capture detailed information about query execution
115
   * including database execution time, response size, and query analysis capabilities.
116
   *
117
   * The method aggregates metrics across all database operations within the query function,
118
   * making it ideal for monitoring resolver performance and detecting performance issues.
119
   *
120
   * @template T - The return type of the query
121
   * @param query - A function that returns a Promise with the query result. Can contain multiple database operations.
122
   * @param onMetadata - Callback function that receives aggregated execution metadata
123
   * @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
124
   * @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
125
   * @param onMetadata.printQueries - Function to analyze and print query execution plans from CLUSTER_STATEMENTS_SUMMARY
126
   * @returns Promise with the query result
127
   *
128
   * @example
129
   * ```typescript
130
   * // Basic usage with performance monitoring
131
   * const result = await forgeSQL.executeWithMetadata(
132
   *   async () => {
133
   *     const users = await forgeSQL.selectFrom(usersTable);
134
   *     const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
135
   *     return { users, orders };
136
   *   },
137
   *   (totalDbExecutionTime, totalResponseSize, printQueries) => {
138
   *     const threshold = 500; // ms baseline for this resolver
139
   *
140
   *     if (totalDbExecutionTime > threshold * 1.5) {
141
   *       console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
142
   *       await printQueries(); // Analyze and print query execution plans
143
   *     } else if (totalDbExecutionTime > threshold) {
144
   *       console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
145
   *     }
146
   *
147
   *     console.log(`DB response size: ${totalResponseSize} bytes`);
148
   *   }
149
   * );
150
   * ```
151
   *
152
   * @example
153
   * ```typescript
154
   * // Resolver with performance monitoring
155
   * resolver.define("fetch", async (req: Request) => {
156
   *   try {
157
   *     return await forgeSQL.executeWithMetadata(
158
   *       async () => {
159
   *         // Resolver logic with multiple queries
160
   *         const users = await forgeSQL.selectFrom(demoUsers);
161
   *         const orders = await forgeSQL.selectFrom(demoOrders)
162
   *           .where(eq(demoOrders.userId, demoUsers.id));
163
   *         return { users, orders };
164
   *       },
165
   *       async (totalDbExecutionTime, totalResponseSize, printQueries) => {
166
   *         const threshold = 500; // ms baseline for this resolver
167
   *
168
   *         if (totalDbExecutionTime > threshold * 1.5) {
169
   *           console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
170
   *           await printQueries(); // Optionally log or capture diagnostics for further analysis
171
   *         } else if (totalDbExecutionTime > threshold) {
172
   *           console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
173
   *         }
174
   *
175
   *         console.log(`DB response size: ${totalResponseSize} bytes`);
176
   *       }
177
   *     );
178
   *   } catch (e) {
179
   *     const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
180
   *     console.error(error, e);
181
   *     throw error;
182
   *   }
183
   * });
184
   * ```
185
   *
186
   * @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueries()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
187
   */
188
  async executeWithMetadata<T>(
189
    query: () => Promise<T>,
190
    onMetadata: (
191
      totalDbExecutionTime: number,
192
      totalResponseSize: number,
193
      printQueriesWithPlan: () => Promise<void>,
194
    ) => Promise<void> | void,
195
  ): Promise<T> {
196
    return metadataQueryContext.run(
1✔
197
      {
198
        totalDbExecutionTime: 0,
199
        totalResponseSize: 0,
200
        beginTime: new Date(),
201
        forgeSQLORM: this,
202
        printQueriesWithPlan: async () => {
203
          return;
×
204
        },
205
      },
206
      async () => {
207
           const result = await query();
1✔
208
          const metadata = await getLastestMetadata();
1✔
209
          try {
1✔
210
               if (metadata) {
1!
211
                   await onMetadata(
1✔
212
                       metadata.totalDbExecutionTime,
213
                       metadata.totalResponseSize,
214
                       metadata.printQueriesWithPlan,
215
                   );
216
               }
217
           } catch (e: any) {
218
              // eslint-disable-next-line no-console
NEW
219
               console.error(
×
220
                 "[ForgeSQLORM][executeWithMetadata] Failed to run onMetadata callback",
221
                 {
222
                   errorMessage: e?.message,
223
                   errorStack: e?.stack,
224
                   totalDbExecutionTime: metadata?.totalDbExecutionTime,
225
                   totalResponseSize: metadata?.totalResponseSize,
226
                   beginTime: metadata?.beginTime,
227
                 },
228
                 e,
229
               );
230
           }
231
           return result;
1✔
232
      },
233
    );
234
  }
235

236
  /**
237
   * Executes operations within a cache context that collects cache eviction events.
238
   * All clearCache calls within the context are collected and executed in batch at the end.
239
   * Queries executed within this context will bypass cache for tables that were marked for clearing.
240
   *
241
   * This is useful for:
242
   * - Batch operations that affect multiple tables
243
   * - Transaction-like operations where you want to clear cache only at the end
244
   * - Performance optimization by reducing cache clear operations
245
   *
246
   * @param cacheContext - Function containing operations that may trigger cache evictions
247
   * @returns Promise that resolves when all operations and cache clearing are complete
248
   *
249
   * @example
250
   * ```typescript
251
   * await forgeSQL.executeWithCacheContext(async () => {
252
   *   await forgeSQL.modifyWithVersioning().insert(users, userData);
253
   *   await forgeSQL.modifyWithVersioning().insert(orders, orderData);
254
   *   // Cache for both users and orders tables will be cleared at the end
255
   * });
256
   * ```
257
   */
258
  executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void> {
259
    return this.executeWithCacheContextAndReturnValue<void>(cacheContext);
6✔
260
  }
261

262
  /**
263
   * Executes operations within a cache context and returns a value.
264
   * All clearCache calls within the context are collected and executed in batch at the end.
265
   * Queries executed within this context will bypass cache for tables that were marked for clearing.
266
   *
267
   * @param cacheContext - Function containing operations that may trigger cache evictions
268
   * @returns Promise that resolves to the return value of the cacheContext function
269
   *
270
   * @example
271
   * ```typescript
272
   * const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
273
   *   await forgeSQL.modifyWithVersioning().insert(users, userData);
274
   *   return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
275
   * });
276
   * ```
277
   */
278
  async executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
279
    return await this.executeWithLocalCacheContextAndReturnValue(
6✔
280
      async () =>
281
        await cacheApplicationContext.run(
6✔
282
          cacheApplicationContext.getStore() ?? { tables: new Set<string>() },
12✔
283
          async () => {
284
            try {
6✔
285
              return await cacheContext();
6✔
286
            } finally {
287
              await clearTablesCache(
6✔
288
                Array.from(cacheApplicationContext.getStore()?.tables ?? []),
6!
289
                this.options,
290
              );
291
            }
292
          },
293
        ),
294
    );
295
  }
296
  /**
297
   * Executes operations within a local cache context and returns a value.
298
   * This provides in-memory caching for select queries within a single request scope.
299
   *
300
   * @param cacheContext - Function containing operations that will benefit from local caching
301
   * @returns Promise that resolves to the return value of the cacheContext function
302
   */
303
  async executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
304
    return await localCacheApplicationContext.run(
18✔
305
      localCacheApplicationContext.getStore() ?? { cache: {} },
36✔
306
      async () => {
307
        return await cacheContext();
18✔
308
      },
309
    );
310
  }
311

312
  /**
313
   * Executes operations within a local cache context.
314
   * This provides in-memory caching for select queries within a single request scope.
315
   *
316
   * @param cacheContext - Function containing operations that will benefit from local caching
317
   * @returns Promise that resolves when all operations are complete
318
   */
319
  executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void> {
320
    return this.executeWithLocalCacheContextAndReturnValue<void>(cacheContext);
9✔
321
  }
322
  /**
323
   * Creates an insert query builder.
324
   *
325
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
326
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
327
   *
328
   * @param table - The table to insert into
329
   * @returns Insert query builder (no versioning, no cache management)
330
   */
331
  insert<TTable extends MySqlTable>(
332
    table: TTable,
333
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
334
    return this.drizzle.insertWithCacheContext(table);
14✔
335
  }
336
  /**
337
   * Creates an insert query builder that automatically evicts cache after execution.
338
   *
339
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
340
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
341
   *
342
   * @param table - The table to insert into
343
   * @returns Insert query builder with automatic cache eviction (no versioning)
344
   */
345
  insertAndEvictCache<TTable extends MySqlTable>(
346
    table: TTable,
347
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
348
    return this.drizzle.insertAndEvictCache(table);
2✔
349
  }
350

351
  /**
352
   * Creates an update query builder that automatically evicts cache after execution.
353
   *
354
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
355
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
356
   *
357
   * @param table - The table to update
358
   * @returns Update query builder with automatic cache eviction (no versioning)
359
   */
360
  updateAndEvictCache<TTable extends MySqlTable>(
361
    table: TTable,
362
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
363
    return this.drizzle.updateAndEvictCache(table);
2✔
364
  }
365

366
  /**
367
   * Creates an update query builder.
368
   *
369
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
370
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
371
   *
372
   * @param table - The table to update
373
   * @returns Update query builder (no versioning, no cache management)
374
   */
375
  update<TTable extends MySqlTable>(
376
    table: TTable,
377
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
378
    return this.drizzle.updateWithCacheContext(table);
16✔
379
  }
380

381
  /**
382
   * Creates a delete query builder.
383
   *
384
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
385
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
386
   *
387
   * @param table - The table to delete from
388
   * @returns Delete query builder (no versioning, no cache management)
389
   */
390
  delete<TTable extends MySqlTable>(
391
    table: TTable,
392
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
393
    return this.drizzle.deleteWithCacheContext(table);
6✔
394
  }
395
  /**
396
   * Creates a delete query builder that automatically evicts cache after execution.
397
   *
398
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
399
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
400
   *
401
   * @param table - The table to delete from
402
   * @returns Delete query builder with automatic cache eviction (no versioning)
403
   */
404
  deleteAndEvictCache<TTable extends MySqlTable>(
405
    table: TTable,
406
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
407
    return this.drizzle.deleteAndEvictCache(table);
2✔
408
  }
409

410
  /**
411
   * Create the modify operations instance.
412
   * @returns modify operations.
413
   */
414
  modifyWithVersioning(): VerioningModificationForgeSQL {
415
    return this.crudOperations;
26✔
416
  }
417

418
  /**
419
   * Returns the singleton instance of ForgeSQLORMImpl.
420
   * @param options - Options for configuring ForgeSQL ORM behavior.
421
   * @returns The singleton instance of ForgeSQLORMImpl.
422
   */
423
  static getInstance(options?: ForgeSqlOrmOptions): ForgeSqlOperation {
424
    ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
81✔
425
    return ForgeSQLORMImpl.instance;
81✔
426
  }
427

428
  /**
429
   * Retrieves the fetch operations instance.
430
   * @returns Fetch operations.
431
   */
432
  fetch(): SchemaSqlForgeSql {
433
    return this.fetchOperations;
6✔
434
  }
435

436
  /**
437
   * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
438
   * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
439
   */
440
  analyze(): SchemaAnalyzeForgeSql {
441
    return this.analyzeOperations;
1✔
442
  }
443

444
  /**
445
   * Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
446
   *
447
   * This method returns operations that use `modifyWithVersioning()` internally, providing:
448
   * - Optimistic locking support
449
   * - Automatic version field management
450
   * - Cache eviction after successful operations
451
   *
452
   * @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
453
   */
454
  modifyWithVersioningAndEvictCache(): ForgeSQLCacheOperations {
455
    return this.cacheOperations;
3✔
456
  }
457

458
  /**
459
   * Returns a Drizzle query builder instance.
460
   *
461
   * ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
462
   * The returned instance should NOT be used for direct database connections or query execution.
463
   * All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
464
   *
465
   * @returns A Drizzle query builder instance for query construction only.
466
   */
467
  getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
468
    selectAliased: SelectAliasedType;
469
    selectAliasedDistinct: SelectAliasedDistinctType;
470
    selectAliasedCacheable: SelectAliasedCacheableType;
471
    selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
472
    insertWithCacheContext: InsertAndEvictCacheType;
473
    insertAndEvictCache: InsertAndEvictCacheType;
474
    updateAndEvictCache: UpdateAndEvictCacheType;
475
    updateWithCacheContext: UpdateAndEvictCacheType;
476
    deleteAndEvictCache: DeleteAndEvictCacheType;
477
    deleteWithCacheContext: DeleteAndEvictCacheType;
478
  } {
479
    return this.drizzle;
15✔
480
  }
481

482
  /**
483
   * Creates a select query with unique field aliases to prevent field name collisions in joins.
484
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
485
   *
486
   * @template TSelection - The type of the selected fields
487
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
488
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
489
   * @throws {Error} If fields parameter is empty
490
   * @example
491
   * ```typescript
492
   * await forgeSQL
493
   *   .select({user: users, order: orders})
494
   *   .from(orders)
495
   *   .innerJoin(users, eq(orders.userId, users.id));
496
   * ```
497
   */
498
  select<TSelection extends SelectedFields>(
499
    fields: TSelection,
500
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
501
    if (!fields) {
21!
502
      throw new Error("fields is empty");
×
503
    }
504
    return this.drizzle.selectAliased(fields);
21✔
505
  }
506

507
  /**
508
   * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
509
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
510
   *
511
   * @template TSelection - The type of the selected fields
512
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
513
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
514
   * @throws {Error} If fields parameter is empty
515
   * @example
516
   * ```typescript
517
   * await forgeSQL
518
   *   .selectDistinct({user: users, order: orders})
519
   *   .from(orders)
520
   *   .innerJoin(users, eq(orders.userId, users.id));
521
   * ```
522
   */
523
  selectDistinct<TSelection extends SelectedFields>(
524
    fields: TSelection,
525
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
526
    if (!fields) {
1!
527
      throw new Error("fields is empty");
×
528
    }
529
    return this.drizzle.selectAliasedDistinct(fields);
1✔
530
  }
531

532
  /**
533
   * Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
534
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
535
   *
536
   * @template TSelection - The type of the selected fields
537
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
538
   * @param {number} cacheTTL - cache ttl optional default is 60 sec.
539
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
540
   * @throws {Error} If fields parameter is empty
541
   * @example
542
   * ```typescript
543
   * await forgeSQL
544
   *   .selectCacheable({user: users, order: orders},60)
545
   *   .from(orders)
546
   *   .innerJoin(users, eq(orders.userId, users.id));
547
   * ```
548
   */
549
  selectCacheable<TSelection extends SelectedFields>(
550
    fields: TSelection,
551
    cacheTTL?: number,
552
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
553
    if (!fields) {
1!
554
      throw new Error("fields is empty");
×
555
    }
556
    return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
1✔
557
  }
558

559
  /**
560
   * Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
561
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
562
   *
563
   * @template TSelection - The type of the selected fields
564
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
565
   * @param {number} cacheTTL - cache ttl optional default is 60 sec.
566
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
567
   * @throws {Error} If fields parameter is empty
568
   * @example
569
   * ```typescript
570
   * await forgeSQL
571
   *   .selectDistinctCacheable({user: users, order: orders}, 60)
572
   *   .from(orders)
573
   *   .innerJoin(users, eq(orders.userId, users.id));
574
   * ```
575
   */
576
  selectDistinctCacheable<TSelection extends SelectedFields>(
577
    fields: TSelection,
578
    cacheTTL?: number,
579
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
580
    if (!fields) {
1!
581
      throw new Error("fields is empty");
×
582
    }
583
    return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
1✔
584
  }
585

586
  /**
587
   * Creates a select query builder for all columns from a table with field aliasing support.
588
   * This is a convenience method that automatically selects all columns from the specified table.
589
   *
590
   * @template T - The type of the table
591
   * @param table - The table to select from
592
   * @returns Select query builder with all table columns and field aliasing support
593
   * @example
594
   * ```typescript
595
   * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
596
   * ```
597
   */
598
  selectFrom<T extends MySqlTable>(table: T) {
599
    return this.drizzle.selectFrom(table);
×
600
  }
601

602
  /**
603
   * Creates a select distinct query builder for all columns from a table with field aliasing support.
604
   * This is a convenience method that automatically selects all distinct columns from the specified table.
605
   *
606
   * @template T - The type of the table
607
   * @param table - The table to select from
608
   * @returns Select distinct query builder with all table columns and field aliasing support
609
   * @example
610
   * ```typescript
611
   * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
612
   * ```
613
   */
614
  selectDistinctFrom<T extends MySqlTable>(table: T) {
615
    return this.drizzle.selectDistinctFrom(table);
×
616
  }
617

618
  /**
619
   * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
620
   * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
621
   *
622
   * @template T - The type of the table
623
   * @param table - The table to select from
624
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
625
   * @returns Select query builder with all table columns, field aliasing, and caching support
626
   * @example
627
   * ```typescript
628
   * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
629
   * ```
630
   */
631
  selectCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
632
    return this.drizzle.selectFromCacheable(table, cacheTTL);
×
633
  }
634

635
  /**
636
   * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
637
   * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
638
   *
639
   * @template T - The type of the table
640
   * @param table - The table to select from
641
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
642
   * @returns Select distinct query builder with all table columns, field aliasing, and caching support
643
   * @example
644
   * ```typescript
645
   * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
646
   * ```
647
   */
648
  selectDistinctCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
649
    return this.drizzle.selectDistinctFromCacheable(table, cacheTTL);
×
650
  }
651

652
  /**
653
   * Executes a raw SQL query with local cache support.
654
   * This method provides local caching for raw SQL queries within the current invocation context.
655
   * Results are cached locally and will be returned from cache on subsequent identical queries.
656
   *
657
   * @param query - The SQL query to execute (SQLWrapper or string)
658
   * @returns Promise with query results
659
   * @example
660
   * ```typescript
661
   * // Using SQLWrapper
662
   * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
663
   *
664
   * // Using string
665
   * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
666
   * ```
667
   */
668
  execute<T>(query: SQLWrapper | string) {
669
    return this.drizzle.executeQuery<T>(query);
1✔
670
  }
671

672
  /**
673
   * Executes a Data Definition Language (DDL) SQL query.
674
   * DDL operations include CREATE, ALTER, DROP, TRUNCATE, and other schema modification statements.
675
   *
676
   * This method is specifically designed for DDL operations and provides:
677
   * - Proper operation type context for DDL queries
678
   * - No caching (DDL operations should not be cached)
679
   * - Direct execution without query optimization
680
   *
681
   * @template T - The expected return type of the query result
682
   * @param query - The DDL SQL query to execute (SQLWrapper or string)
683
   * @returns Promise with query results
684
   * @throws {Error} If the DDL operation fails
685
   *
686
   * @example
687
   * ```typescript
688
   * // Create a new table
689
   * await forgeSQL.executeDDL(`
690
   *   CREATE TABLE users (
691
   *     id INT PRIMARY KEY AUTO_INCREMENT,
692
   *     name VARCHAR(255) NOT NULL,
693
   *     email VARCHAR(255) UNIQUE
694
   *   )
695
   * `);
696
   *
697
   * // Alter table structure
698
   * await forgeSQL.executeDDL(sql`
699
   *   ALTER TABLE users
700
   *   ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
701
   * `);
702
   *
703
   * // Drop a table
704
   * await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
705
   * ```
706
   */
707
  async executeDDL<T>(query: SQLWrapper | string) {
708
    return this.executeDDLActions(async () => this.drizzle.executeQuery<T>(query));
2✔
709
  }
710

711
  /**
712
   * Executes a series of actions within a DDL operation context.
713
   * This method provides a way to execute regular SQL queries that should be treated
714
   * as DDL operations, ensuring proper operation type context for performance monitoring.
715
   *
716
   * This method is useful for:
717
   * - Executing regular SQL queries in DDL context for monitoring purposes
718
   * - Wrapping non-DDL operations that should be treated as DDL for analysis
719
   * - Ensuring proper operation type context for complex workflows
720
   * - Maintaining DDL operation context across multiple function calls
721
   *
722
   * @template T - The return type of the actions function
723
   * @param actions - Function containing SQL operations to execute in DDL context
724
   * @returns Promise that resolves to the return value of the actions function
725
   *
726
   * @example
727
   * ```typescript
728
   * // Execute regular SQL queries in DDL context for monitoring
729
   * await forgeSQL.executeDDLActions(async () => {
730
   *   const slowQueries = await forgeSQL.execute(`
731
   *     SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
732
   *     WHERE AVG_LATENCY > 1000000
733
   *   `);
734
   *   return slowQueries;
735
   * });
736
   *
737
   * // Execute complex analysis queries in DDL context
738
   * const result = await forgeSQL.executeDDLActions(async () => {
739
   *   const tableInfo = await forgeSQL.execute("SHOW TABLES");
740
   *   const performanceData = await forgeSQL.execute(`
741
   *     SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
742
   *     WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
743
   *   `);
744
   *   return { tableInfo, performanceData };
745
   * });
746
   *
747
   * // Execute monitoring queries with error handling
748
   * try {
749
   *   await forgeSQL.executeDDLActions(async () => {
750
   *     const metrics = await forgeSQL.execute(`
751
   *       SELECT COUNT(*) as query_count
752
   *       FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
753
   *     `);
754
   *     console.log(`Total queries: ${metrics[0].query_count}`);
755
   *   });
756
   * } catch (error) {
757
   *   console.error("Monitoring query failed:", error);
758
   * }
759
   * ```
760
   */
761
  async executeDDLActions<T>(actions: () => Promise<T>): Promise<T> {
762
    return operationTypeQueryContext.run({ operationType: "DDL" }, async () => actions());
2✔
763
  }
764

765
  /**
766
   * Executes a raw SQL query with both local and global cache support.
767
   * This method provides comprehensive caching for raw SQL queries:
768
   * - Local cache: Within the current invocation context
769
   * - Global cache: Cross-invocation caching using @forge/kvs
770
   *
771
   * @param query - The SQL query to execute (SQLWrapper or string)
772
   * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
773
   * @returns Promise with query results
774
   * @example
775
   * ```typescript
776
   * // Using SQLWrapper with custom TTL
777
   * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
778
   *
779
   * // Using string with default TTL
780
   * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
781
   * ```
782
   */
783
  executeCacheable<T>(query: SQLWrapper | string, cacheTtl?: number) {
784
    return this.drizzle.executeQueryCacheable<T>(query, cacheTtl);
2✔
785
  }
786

787
  /**
788
   * Creates a Common Table Expression (CTE) builder for complex queries.
789
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
790
   *
791
   * @returns WithBuilder for creating CTEs
792
   * @example
793
   * ```typescript
794
   * const withQuery = forgeSQL.$with('userStats').as(
795
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
796
   *     .from(users)
797
   *     .groupBy(users.id)
798
   * );
799
   * ```
800
   */
801
  get $with() {
802
    return this.drizzle.$with;
×
803
  }
804

805
  /**
806
   * Creates a query builder that uses Common Table Expressions (CTEs).
807
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
808
   *
809
   * @param queries - Array of CTE queries created with $with()
810
   * @returns Query builder with CTE support
811
   * @example
812
   * ```typescript
813
   * const withQuery = forgeSQL.$with('userStats').as(
814
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
815
   *     .from(users)
816
   *     .groupBy(users.id)
817
   * );
818
   *
819
   * const result = await forgeSQL.with(withQuery)
820
   *   .select({ userId: withQuery.userId, count: withQuery.count })
821
   *   .from(withQuery);
822
   * ```
823
   */
824
  with(...queries: WithSubquery[]) {
825
    return this.drizzle.with(...queries);
×
826
  }
827
}
828

829
/**
830
 * Public class that acts as a wrapper around the private ForgeSQLORMImpl.
831
 * Provides a clean interface for working with Forge SQL and Drizzle ORM.
832
 */
833
class ForgeSQLORM implements ForgeSqlOperation {
834
  private readonly ormInstance: ForgeSqlOperation;
835

836
  constructor(options?: ForgeSqlOrmOptions) {
837
    this.ormInstance = ForgeSQLORMImpl.getInstance(options);
81✔
838
  }
839

840
  /**
841
   * Executes a query and provides access to execution metadata with performance monitoring.
842
   * This method allows you to capture detailed information about query execution
843
   * including database execution time, response size, and query analysis capabilities.
844
   *
845
   * The method aggregates metrics across all database operations within the query function,
846
   * making it ideal for monitoring resolver performance and detecting performance issues.
847
   *
848
   * @template T - The return type of the query
849
   * @param query - A function that returns a Promise with the query result. Can contain multiple database operations.
850
   * @param onMetadata - Callback function that receives aggregated execution metadata
851
   * @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
852
   * @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
853
   * @param onMetadata.printQueries - Function to analyze and print query execution plans from CLUSTER_STATEMENTS_SUMMARY
854
   * @returns Promise with the query result
855
   *
856
   * @example
857
   * ```typescript
858
   * // Basic usage with performance monitoring
859
   * const result = await forgeSQL.executeWithMetadata(
860
   *   async () => {
861
   *     const users = await forgeSQL.selectFrom(usersTable);
862
   *     const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
863
   *     return { users, orders };
864
   *   },
865
   *   (totalDbExecutionTime, totalResponseSize, printQueries) => {
866
   *     const threshold = 500; // ms baseline for this resolver
867
   *
868
   *     if (totalDbExecutionTime > threshold * 1.5) {
869
   *       console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
870
   *       await printQueries(); // Analyze and print query execution plans
871
   *     } else if (totalDbExecutionTime > threshold) {
872
   *       console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
873
   *     }
874
   *
875
   *     console.log(`DB response size: ${totalResponseSize} bytes`);
876
   *   }
877
   * );
878
   * ```
879
   *
880
   * @example
881
   * ```typescript
882
   * // Resolver with performance monitoring
883
   * resolver.define("fetch", async (req: Request) => {
884
   *   try {
885
   *     return await forgeSQL.executeWithMetadata(
886
   *       async () => {
887
   *         // Resolver logic with multiple queries
888
   *         const users = await forgeSQL.selectFrom(demoUsers);
889
   *         const orders = await forgeSQL.selectFrom(demoOrders)
890
   *           .where(eq(demoOrders.userId, demoUsers.id));
891
   *         return { users, orders };
892
   *       },
893
   *       async (totalDbExecutionTime, totalResponseSize, printQueries) => {
894
   *         const threshold = 500; // ms baseline for this resolver
895
   *
896
   *         if (totalDbExecutionTime > threshold * 1.5) {
897
   *           console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
898
   *           await printQueries(); // Optionally log or capture diagnostics for further analysis
899
   *         } else if (totalDbExecutionTime > threshold) {
900
   *           console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
901
   *         }
902
   *
903
   *         console.log(`DB response size: ${totalResponseSize} bytes`);
904
   *       }
905
   *     );
906
   *   } catch (e) {
907
   *     const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
908
   *     console.error(error, e);
909
   *     throw error;
910
   *   }
911
   * });
912
   * ```
913
   *
914
   * @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueries()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
915
   */
916
  async executeWithMetadata<T>(
917
    query: () => Promise<T>,
918
    onMetadata: (
919
      totalDbExecutionTime: number,
920
      totalResponseSize: number,
921
      printQueriesWithPlan: () => Promise<void>,
922
    ) => Promise<void> | void,
923
  ): Promise<T> {
924
    return this.ormInstance.executeWithMetadata(query, onMetadata);
1✔
925
  }
926

927
  selectCacheable<TSelection extends SelectedFields>(
928
    fields: TSelection,
929
    cacheTTL?: number,
930
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
931
    return this.ormInstance.selectCacheable(fields, cacheTTL);
1✔
932
  }
933

934
  selectDistinctCacheable<TSelection extends SelectedFields>(
935
    fields: TSelection,
936
    cacheTTL?: number,
937
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
938
    return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
1✔
939
  }
940

941
  /**
942
   * Creates a select query builder for all columns from a table with field aliasing support.
943
   * This is a convenience method that automatically selects all columns from the specified table.
944
   *
945
   * @template T - The type of the table
946
   * @param table - The table to select from
947
   * @returns Select query builder with all table columns and field aliasing support
948
   * @example
949
   * ```typescript
950
   * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
951
   * ```
952
   */
953
  selectFrom<T extends MySqlTable>(table: T) {
954
    return this.ormInstance.getDrizzleQueryBuilder().selectFrom(table);
1✔
955
  }
956

957
  /**
958
   * Creates a select distinct query builder for all columns from a table with field aliasing support.
959
   * This is a convenience method that automatically selects all distinct columns from the specified table.
960
   *
961
   * @template T - The type of the table
962
   * @param table - The table to select from
963
   * @returns Select distinct query builder with all table columns and field aliasing support
964
   * @example
965
   * ```typescript
966
   * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
967
   * ```
968
   */
969
  selectDistinctFrom<T extends MySqlTable>(table: T) {
970
    return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFrom(table);
1✔
971
  }
972

973
  /**
974
   * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
975
   * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
976
   *
977
   * @template T - The type of the table
978
   * @param table - The table to select from
979
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
980
   * @returns Select query builder with all table columns, field aliasing, and caching support
981
   * @example
982
   * ```typescript
983
   * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
984
   * ```
985
   */
986
  selectCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
987
    return this.ormInstance.getDrizzleQueryBuilder().selectFromCacheable(table, cacheTTL);
1✔
988
  }
989

990
  /**
991
   * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
992
   * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
993
   *
994
   * @template T - The type of the table
995
   * @param table - The table to select from
996
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
997
   * @returns Select distinct query builder with all table columns, field aliasing, and caching support
998
   * @example
999
   * ```typescript
1000
   * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
1001
   * ```
1002
   */
1003
  selectDistinctCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
1004
    return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFromCacheable(table, cacheTTL);
1✔
1005
  }
1006

1007
  executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void> {
1008
    return this.ormInstance.executeWithCacheContext(cacheContext);
6✔
1009
  }
1010
  executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
1011
    return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
×
1012
  }
1013
  /**
1014
   * Executes operations within a local cache context.
1015
   * This provides in-memory caching for select queries within a single request scope.
1016
   *
1017
   * @param cacheContext - Function containing operations that will benefit from local caching
1018
   * @returns Promise that resolves when all operations are complete
1019
   */
1020
  executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void> {
1021
    return this.ormInstance.executeWithLocalContext(cacheContext);
9✔
1022
  }
1023

1024
  /**
1025
   * Executes operations within a local cache context and returns a value.
1026
   * This provides in-memory caching for select queries within a single request scope.
1027
   *
1028
   * @param cacheContext - Function containing operations that will benefit from local caching
1029
   * @returns Promise that resolves to the return value of the cacheContext function
1030
   */
1031
  executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
1032
    return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
3✔
1033
  }
1034

1035
  /**
1036
   * Creates an insert query builder.
1037
   *
1038
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1039
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
1040
   *
1041
   * @param table - The table to insert into
1042
   * @returns Insert query builder (no versioning, no cache management)
1043
   */
1044
  insert<TTable extends MySqlTable>(
1045
    table: TTable,
1046
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
1047
    return this.ormInstance.insert(table);
5✔
1048
  }
1049

1050
  /**
1051
   * Creates an insert query builder that automatically evicts cache after execution.
1052
   *
1053
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1054
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
1055
   *
1056
   * @param table - The table to insert into
1057
   * @returns Insert query builder with automatic cache eviction (no versioning)
1058
   */
1059
  insertAndEvictCache<TTable extends MySqlTable>(
1060
    table: TTable,
1061
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
1062
    return this.ormInstance.insertAndEvictCache(table);
2✔
1063
  }
1064

1065
  /**
1066
   * Creates an update query builder.
1067
   *
1068
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1069
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
1070
   *
1071
   * @param table - The table to update
1072
   * @returns Update query builder (no versioning, no cache management)
1073
   */
1074
  update<TTable extends MySqlTable>(
1075
    table: TTable,
1076
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
1077
    return this.ormInstance.update(table);
3✔
1078
  }
1079

1080
  /**
1081
   * Creates an update query builder that automatically evicts cache after execution.
1082
   *
1083
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1084
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
1085
   *
1086
   * @param table - The table to update
1087
   * @returns Update query builder with automatic cache eviction (no versioning)
1088
   */
1089
  updateAndEvictCache<TTable extends MySqlTable>(
1090
    table: TTable,
1091
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
1092
    return this.ormInstance.updateAndEvictCache(table);
2✔
1093
  }
1094

1095
  /**
1096
   * Creates a delete query builder.
1097
   *
1098
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1099
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
1100
   *
1101
   * @param table - The table to delete from
1102
   * @returns Delete query builder (no versioning, no cache management)
1103
   */
1104
  delete<TTable extends MySqlTable>(
1105
    table: TTable,
1106
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
1107
    return this.ormInstance.delete(table);
3✔
1108
  }
1109

1110
  /**
1111
   * Creates a delete query builder that automatically evicts cache after execution.
1112
   *
1113
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1114
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
1115
   *
1116
   * @param table - The table to delete from
1117
   * @returns Delete query builder with automatic cache eviction (no versioning)
1118
   */
1119
  deleteAndEvictCache<TTable extends MySqlTable>(
1120
    table: TTable,
1121
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
1122
    return this.ormInstance.deleteAndEvictCache(table);
2✔
1123
  }
1124

1125
  /**
1126
   * Creates a select query with unique field aliases to prevent field name collisions in joins.
1127
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
1128
   *
1129
   * @template TSelection - The type of the selected fields
1130
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
1131
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
1132
   * @throws {Error} If fields parameter is empty
1133
   * @example
1134
   * ```typescript
1135
   * await forgeSQL
1136
   *   .select({user: users, order: orders})
1137
   *   .from(orders)
1138
   *   .innerJoin(users, eq(orders.userId, users.id));
1139
   * ```
1140
   */
1141
  select<TSelection extends SelectedFields>(
1142
    fields: TSelection,
1143
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1144
    return this.ormInstance.select(fields);
20✔
1145
  }
1146

1147
  /**
1148
   * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
1149
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
1150
   *
1151
   * @template TSelection - The type of the selected fields
1152
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
1153
   * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases
1154
   * @throws {Error} If fields parameter is empty
1155
   * @example
1156
   * ```typescript
1157
   * await forgeSQL
1158
   *   .selectDistinct({user: users, order: orders})
1159
   *   .from(orders)
1160
   *   .innerJoin(users, eq(orders.userId, users.id));
1161
   * ```
1162
   */
1163
  selectDistinct<TSelection extends SelectedFields>(
1164
    fields: TSelection,
1165
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1166
    return this.ormInstance.selectDistinct(fields);
1✔
1167
  }
1168

1169
  /**
1170
   * Proxies the `modify` method from `ForgeSQLORMImpl`.
1171
   * @returns Modify operations.
1172
   */
1173
  modifyWithVersioning(): VerioningModificationForgeSQL {
1174
    return this.ormInstance.modifyWithVersioning();
25✔
1175
  }
1176

1177
  /**
1178
   * Proxies the `fetch` method from `ForgeSQLORMImpl`.
1179
   * @returns Fetch operations.
1180
   */
1181
  fetch(): SchemaSqlForgeSql {
1182
    return this.ormInstance.fetch();
5✔
1183
  }
1184

1185
  /**
1186
   * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
1187
   * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
1188
   */
1189
  analyze(): SchemaAnalyzeForgeSql {
1190
    return this.ormInstance.analyze();
1✔
1191
  }
1192

1193
  /**
1194
   * Provides schema-level SQL cacheable operations with type safety.
1195
   * @returns {ForgeSQLCacheOperations} Interface for executing schema-bound SQL queries
1196
   */
1197
  modifyWithVersioningAndEvictCache(): ForgeSQLCacheOperations {
1198
    return this.ormInstance.modifyWithVersioningAndEvictCache();
3✔
1199
  }
1200

1201
  /**
1202
   * Returns a Drizzle query builder instance.
1203
   *
1204
   * @returns A Drizzle query builder instance for query construction only.
1205
   */
1206
  getDrizzleQueryBuilder() {
1207
    return this.ormInstance.getDrizzleQueryBuilder();
8✔
1208
  }
1209

1210
  /**
1211
   * Executes a raw SQL query with local cache support.
1212
   * This method provides local caching for raw SQL queries within the current invocation context.
1213
   * Results are cached locally and will be returned from cache on subsequent identical queries.
1214
   *
1215
   * @param query - The SQL query to execute (SQLWrapper or string)
1216
   * @returns Promise with query results
1217
   * @example
1218
   * ```typescript
1219
   * // Using SQLWrapper
1220
   * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
1221
   *
1222
   * // Using string
1223
   * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
1224
   * ```
1225
   */
1226
  execute<T>(
1227
    query: SQLWrapper | string,
1228
  ): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>> {
1229
    return this.ormInstance.execute(query);
1✔
1230
  }
1231

1232
  /**
1233
   * Executes a Data Definition Language (DDL) SQL query.
1234
   * DDL operations include CREATE, ALTER, DROP, TRUNCATE, and other schema modification statements.
1235
   *
1236
   * This method is specifically designed for DDL operations and provides:
1237
   * - Proper operation type context for DDL queries
1238
   * - No caching (DDL operations should not be cached)
1239
   * - Direct execution without query optimization
1240
   *
1241
   * @template T - The expected return type of the query result
1242
   * @param query - The DDL SQL query to execute (SQLWrapper or string)
1243
   * @returns Promise with query results
1244
   * @throws {Error} If the DDL operation fails
1245
   *
1246
   * @example
1247
   * ```typescript
1248
   * // Create a new table
1249
   * await forgeSQL.executeDDL(`
1250
   *   CREATE TABLE users (
1251
   *     id INT PRIMARY KEY AUTO_INCREMENT,
1252
   *     name VARCHAR(255) NOT NULL,
1253
   *     email VARCHAR(255) UNIQUE
1254
   *   )
1255
   * `);
1256
   *
1257
   * // Alter table structure
1258
   * await forgeSQL.executeDDL(sql`
1259
   *   ALTER TABLE users
1260
   *   ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1261
   * `);
1262
   *
1263
   * // Drop a table
1264
   * await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
1265
   * ```
1266
   */
1267
  executeDDL(query: SQLWrapper | string) {
1268
    return this.ormInstance.executeDDL(query);
2✔
1269
  }
1270

1271
  /**
1272
   * Executes a series of actions within a DDL operation context.
1273
   * This method provides a way to execute regular SQL queries that should be treated
1274
   * as DDL operations, ensuring proper operation type context for performance monitoring.
1275
   *
1276
   * This method is useful for:
1277
   * - Executing regular SQL queries in DDL context for monitoring purposes
1278
   * - Wrapping non-DDL operations that should be treated as DDL for analysis
1279
   * - Ensuring proper operation type context for complex workflows
1280
   * - Maintaining DDL operation context across multiple function calls
1281
   *
1282
   * @template T - The return type of the actions function
1283
   * @param actions - Function containing SQL operations to execute in DDL context
1284
   * @returns Promise that resolves to the return value of the actions function
1285
   *
1286
   * @example
1287
   * ```typescript
1288
   * // Execute regular SQL queries in DDL context for monitoring
1289
   * await forgeSQL.executeDDLActions(async () => {
1290
   *   const slowQueries = await forgeSQL.execute(`
1291
   *     SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1292
   *     WHERE AVG_LATENCY > 1000000
1293
   *   `);
1294
   *   return slowQueries;
1295
   * });
1296
   *
1297
   * // Execute complex analysis queries in DDL context
1298
   * const result = await forgeSQL.executeDDLActions(async () => {
1299
   *   const tableInfo = await forgeSQL.execute("SHOW TABLES");
1300
   *   const performanceData = await forgeSQL.execute(`
1301
   *     SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
1302
   *     WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
1303
   *   `);
1304
   *   return { tableInfo, performanceData };
1305
   * });
1306
   *
1307
   * // Execute monitoring queries with error handling
1308
   * try {
1309
   *   await forgeSQL.executeDDLActions(async () => {
1310
   *     const metrics = await forgeSQL.execute(`
1311
   *       SELECT COUNT(*) as query_count
1312
   *       FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
1313
   *     `);
1314
   *     console.log(`Total queries: ${metrics[0].query_count}`);
1315
   *   });
1316
   * } catch (error) {
1317
   *   console.error("Monitoring query failed:", error);
1318
   * }
1319
   * ```
1320
   */
1321
  executeDDLActions<T>(actions: () => Promise<T>): Promise<T> {
1322
    return this.ormInstance.executeDDLActions(actions);
×
1323
  }
1324

1325
  /**
1326
   * Executes a raw SQL query with both local and global cache support.
1327
   * This method provides comprehensive caching for raw SQL queries:
1328
   * - Local cache: Within the current invocation context
1329
   * - Global cache: Cross-invocation caching using @forge/kvs
1330
   *
1331
   * @param query - The SQL query to execute (SQLWrapper or string)
1332
   * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
1333
   * @returns Promise with query results
1334
   * @example
1335
   * ```typescript
1336
   * // Using SQLWrapper with custom TTL
1337
   * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
1338
   *
1339
   * // Using string with default TTL
1340
   * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
1341
   * ```
1342
   */
1343
  executeCacheable(query: SQLWrapper | string, cacheTtl?: number) {
1344
    return this.ormInstance.executeCacheable(query, cacheTtl);
2✔
1345
  }
1346

1347
  /**
1348
   * Creates a Common Table Expression (CTE) builder for complex queries.
1349
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
1350
   *
1351
   * @returns WithBuilder for creating CTEs
1352
   * @example
1353
   * ```typescript
1354
   * const withQuery = forgeSQL.$with('userStats').as(
1355
   *   forgeSQL.getDrizzleQueryBuilder().select({ userId: users.id, count: sql<number>`count(*)` })
1356
   *     .from(users)
1357
   *     .groupBy(users.id)
1358
   * );
1359
   * ```
1360
   */
1361
  get $with() {
1362
    return this.ormInstance.getDrizzleQueryBuilder().$with;
1✔
1363
  }
1364

1365
  /**
1366
   * Creates a query builder that uses Common Table Expressions (CTEs).
1367
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
1368
   *
1369
   * @param queries - Array of CTE queries created with $with()
1370
   * @returns Query builder with CTE support
1371
   * @example
1372
   * ```typescript
1373
   * const withQuery = forgeSQL.$with('userStats').as(
1374
   *   forgeSQL.getDrizzleQueryBuilder().select({ userId: users.id, count: sql<number>`count(*)` })
1375
   *     .from(users)
1376
   *     .groupBy(users.id)
1377
   * );
1378
   *
1379
   * const result = await forgeSQL.with(withQuery)
1380
   *   .select({ userId: withQuery.userId, count: withQuery.count })
1381
   *   .from(withQuery);
1382
   * ```
1383
   */
1384
  with(...queries: WithSubquery[]) {
1385
    return this.ormInstance.getDrizzleQueryBuilder().with(...queries);
1✔
1386
  }
1387
}
1388

1389
export default ForgeSQLORM;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc