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

vzakharchenko / forge-sql-orm / 17727707721

15 Sep 2025 09:01AM UTC coverage: 87.957% (-0.2%) from 88.18%
17727707721

Pull #630

github

web-flow
Merge 90298614e into 6d2182d74
Pull Request #630: fix examples

463 of 555 branches covered (83.42%)

Branch coverage included in aggregate %.

113 of 146 new or added lines in 2 files covered. (77.4%)

1991 of 2235 relevant lines covered (89.08%)

15.16 hits per line

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

87.5
/src/core/ForgeSQLORM.ts
1
import { ForgeSQLCrudOperations } from "./ForgeSQLCrudOperations";
1✔
2
import {
3
  VerioningModificationForgeSQL,
4
  ForgeSqlOperation,
5
  ForgeSqlOrmOptions,
6
  SchemaAnalyzeForgeSql,
7
  SchemaSqlForgeSql,
8
} from "./ForgeSQLQueryBuilder";
9
import { ForgeSQLSelectOperations } from "./ForgeSQLSelectOperations";
1✔
10
import {
1✔
11
  drizzle,
12
  MySqlRemoteDatabase,
13
  MySqlRemotePreparedQueryHKT,
14
  MySqlRemoteQueryResultHKT,
15
} from "drizzle-orm/mysql-proxy";
16
import { createForgeDriverProxy } from "../utils/forgeDriverProxy";
1✔
17
import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
18
import { MySqlSelectBuilder } from "drizzle-orm/mysql-core";
19
import {
1✔
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";
1✔
30
import { ForgeSQLCacheOperations } from "./ForgeSQLCacheOperations";
1✔
31
import type { 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";
1✔
38
import { clearTablesCache } from "../utils/cacheUtils";
1✔
39
import { SQLWrapper } from "drizzle-orm/sql/sql";
40
import { WithSubquery } from "drizzle-orm/subquery";
41

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

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

102
  /**
103
   * Executes operations within a cache context that collects cache eviction events.
104
   * All clearCache calls within the context are collected and executed in batch at the end.
105
   * Queries executed within this context will bypass cache for tables that were marked for clearing.
106
   *
107
   * This is useful for:
108
   * - Batch operations that affect multiple tables
109
   * - Transaction-like operations where you want to clear cache only at the end
110
   * - Performance optimization by reducing cache clear operations
111
   *
112
   * @param cacheContext - Function containing operations that may trigger cache evictions
113
   * @returns Promise that resolves when all operations and cache clearing are complete
114
   *
115
   * @example
116
   * ```typescript
117
   * await forgeSQL.executeWithCacheContext(async () => {
118
   *   await forgeSQL.modifyWithVersioning().insert(users, userData);
119
   *   await forgeSQL.modifyWithVersioning().insert(orders, orderData);
120
   *   // Cache for both users and orders tables will be cleared at the end
121
   * });
122
   * ```
123
   */
124
  executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void> {
6✔
125
    return this.executeWithCacheContextAndReturnValue<void>(cacheContext);
6✔
126
  }
6✔
127

128
  /**
129
   * Executes operations within a cache context and returns a value.
130
   * All clearCache calls within the context are collected and executed in batch at the end.
131
   * Queries executed within this context will bypass cache for tables that were marked for clearing.
132
   *
133
   * @param cacheContext - Function containing operations that may trigger cache evictions
134
   * @returns Promise that resolves to the return value of the cacheContext function
135
   *
136
   * @example
137
   * ```typescript
138
   * const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
139
   *   await forgeSQL.modifyWithVersioning().insert(users, userData);
140
   *   return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
141
   * });
142
   * ```
143
   */
144
  async executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
6✔
145
    return await this.executeWithLocalCacheContextAndReturnValue(
6✔
146
      async () =>
6✔
147
        await cacheApplicationContext.run(cacheApplicationContext.getStore() ?? { tables: new Set<string>() }, async () => {
6✔
148
          try {
6✔
149
            return await cacheContext();
6✔
150
          } finally {
6✔
151
            await clearTablesCache(
6✔
152
              Array.from(cacheApplicationContext.getStore()?.tables ?? []),
6!
153
              this.options,
6✔
154
            );
6✔
155
          }
6✔
156
        }),
6✔
157
    );
6✔
158
  }
6✔
159
  /**
160
   * Executes operations within a local cache context and returns a value.
161
   * This provides in-memory caching for select queries within a single request scope.
162
   * 
163
   * @param cacheContext - Function containing operations that will benefit from local caching
164
   * @returns Promise that resolves to the return value of the cacheContext function
165
   */
166
  async executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
6✔
167
    return await localCacheApplicationContext.run(localCacheApplicationContext.getStore() ?? { cache: {} }, async () => {
18✔
168
      return await cacheContext();
18✔
169
    });
18✔
170
  }
18✔
171

172
  /**
173
   * Executes operations within a local cache context.
174
   * This provides in-memory caching for select queries within a single request scope.
175
   * 
176
   * @param cacheContext - Function containing operations that will benefit from local caching
177
   * @returns Promise that resolves when all operations are complete
178
   */
179
  executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void> {
6✔
180
    return this.executeWithLocalCacheContextAndReturnValue<void>(cacheContext);
9✔
181
  }
9✔
182
  /**
183
   * Creates an insert query builder.
184
   *
185
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
186
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
187
   *
188
   * @param table - The table to insert into
189
   * @returns Insert query builder (no versioning, no cache management)
190
   */
191
  insert<TTable extends MySqlTable>(
6✔
192
    table: TTable,
14✔
193
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
14✔
194
    return this.drizzle.insertWithCacheContext(table);
14✔
195
  }
14✔
196
  /**
197
   * Creates an insert query builder that automatically evicts cache after execution.
198
   *
199
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
200
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
201
   *
202
   * @param table - The table to insert into
203
   * @returns Insert query builder with automatic cache eviction (no versioning)
204
   */
205
  insertAndEvictCache<TTable extends MySqlTable>(
6✔
206
    table: TTable,
2✔
207
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
2✔
208
    return this.drizzle.insertAndEvictCache(table);
2✔
209
  }
2✔
210

211
  /**
212
   * Creates an update query builder that automatically evicts cache after execution.
213
   *
214
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
215
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
216
   *
217
   * @param table - The table to update
218
   * @returns Update query builder with automatic cache eviction (no versioning)
219
   */
220
  updateAndEvictCache<TTable extends MySqlTable>(
6✔
221
    table: TTable,
2✔
222
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
2✔
223
    return this.drizzle.updateAndEvictCache(table);
2✔
224
  }
2✔
225

226
  /**
227
   * Creates an update query builder.
228
   *
229
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
230
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
231
   *
232
   * @param table - The table to update
233
   * @returns Update query builder (no versioning, no cache management)
234
   */
235
  update<TTable extends MySqlTable>(
6✔
236
    table: TTable,
16✔
237
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
16✔
238
    return this.drizzle.updateWithCacheContext(table);
16✔
239
  }
16✔
240

241
  /**
242
   * Creates a delete query builder.
243
   *
244
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
245
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
246
   *
247
   * @param table - The table to delete from
248
   * @returns Delete query builder (no versioning, no cache management)
249
   */
250
  delete<TTable extends MySqlTable>(
6✔
251
    table: TTable,
6✔
252
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
6✔
253
    return this.drizzle.deleteWithCacheContext(table);
6✔
254
  }
6✔
255
  /**
256
   * Creates a delete query builder that automatically evicts cache after execution.
257
   *
258
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
259
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
260
   *
261
   * @param table - The table to delete from
262
   * @returns Delete query builder with automatic cache eviction (no versioning)
263
   */
264
  deleteAndEvictCache<TTable extends MySqlTable>(
6✔
265
    table: TTable,
2✔
266
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
2✔
267
    return this.drizzle.deleteAndEvictCache(table);
2✔
268
  }
2✔
269

270
  /**
271
   * Create the modify operations instance.
272
   * @returns modify operations.
273
   */
274
  modifyWithVersioning(): VerioningModificationForgeSQL {
6✔
275
    return this.crudOperations;
26✔
276
  }
26✔
277

278
  /**
279
   * Returns the singleton instance of ForgeSQLORMImpl.
280
   * @param options - Options for configuring ForgeSQL ORM behavior.
281
   * @returns The singleton instance of ForgeSQLORMImpl.
282
   */
283
  static getInstance(options?: ForgeSqlOrmOptions): ForgeSqlOperation {
6✔
284
    ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
77✔
285
    return ForgeSQLORMImpl.instance;
77✔
286
  }
77✔
287

288
  /**
289
   * Retrieves the fetch operations instance.
290
   * @returns Fetch operations.
291
   */
292
  fetch(): SchemaSqlForgeSql {
6✔
293
    return this.fetchOperations;
6✔
294
  }
6✔
295

296
  /**
297
   * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
298
   * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
299
   */
300
  analyze(): SchemaAnalyzeForgeSql {
6✔
301
    return this.analyzeOperations;
1✔
302
  }
1✔
303

304
  /**
305
   * Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
306
   *
307
   * This method returns operations that use `modifyWithVersioning()` internally, providing:
308
   * - Optimistic locking support
309
   * - Automatic version field management
310
   * - Cache eviction after successful operations
311
   *
312
   * @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
313
   */
314
  modifyWithVersioningAndEvictCache(): ForgeSQLCacheOperations {
6✔
315
    return this.cacheOperations;
3✔
316
  }
3✔
317

318
  /**
319
   * Returns a Drizzle query builder instance.
320
   *
321
   * ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
322
   * The returned instance should NOT be used for direct database connections or query execution.
323
   * All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
324
   *
325
   * @returns A Drizzle query builder instance for query construction only.
326
   */
327
  getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
6✔
328
    selectAliased: SelectAliasedType;
329
    selectAliasedDistinct: SelectAliasedDistinctType;
330
    selectAliasedCacheable: SelectAliasedCacheableType;
331
    selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
332
    insertWithCacheContext: InsertAndEvictCacheType;
333
    insertAndEvictCache: InsertAndEvictCacheType;
334
    updateAndEvictCache: UpdateAndEvictCacheType;
335
    updateWithCacheContext: UpdateAndEvictCacheType;
336
    deleteAndEvictCache: DeleteAndEvictCacheType;
337
    deleteWithCacheContext: DeleteAndEvictCacheType;
338
  } {
16✔
339
    return this.drizzle;
16✔
340
  }
16✔
341

342
  /**
343
   * Creates a select query with unique field aliases to prevent field name collisions in joins.
344
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
345
   *
346
   * @template TSelection - The type of the selected fields
347
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
348
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
349
   * @throws {Error} If fields parameter is empty
350
   * @example
351
   * ```typescript
352
   * await forgeSQL
353
   *   .select({user: users, order: orders})
354
   *   .from(orders)
355
   *   .innerJoin(users, eq(orders.userId, users.id));
356
   * ```
357
   */
358
  select<TSelection extends SelectedFields>(
6✔
359
    fields: TSelection,
20✔
360
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
20✔
361
    if (!fields) {
20!
362
      throw new Error("fields is empty");
×
363
    }
×
364
    return this.drizzle.selectAliased(fields);
20✔
365
  }
20✔
366

367
  /**
368
   * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
369
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
370
   *
371
   * @template TSelection - The type of the selected fields
372
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
373
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
374
   * @throws {Error} If fields parameter is empty
375
   * @example
376
   * ```typescript
377
   * await forgeSQL
378
   *   .selectDistinct({user: users, order: orders})
379
   *   .from(orders)
380
   *   .innerJoin(users, eq(orders.userId, users.id));
381
   * ```
382
   */
383
  selectDistinct<TSelection extends SelectedFields>(
6✔
384
    fields: TSelection,
1✔
385
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1✔
386
    if (!fields) {
1!
387
      throw new Error("fields is empty");
×
388
    }
×
389
    return this.drizzle.selectAliasedDistinct(fields);
1✔
390
  }
1✔
391

392
  /**
393
   * Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
394
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
395
   *
396
   * @template TSelection - The type of the selected fields
397
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
398
   * @param {number} cacheTTL - cache ttl optional default is 60 sec.
399
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
400
   * @throws {Error} If fields parameter is empty
401
   * @example
402
   * ```typescript
403
   * await forgeSQL
404
   *   .selectCacheable({user: users, order: orders},60)
405
   *   .from(orders)
406
   *   .innerJoin(users, eq(orders.userId, users.id));
407
   * ```
408
   */
409
  selectCacheable<TSelection extends SelectedFields>(
6✔
410
    fields: TSelection,
1✔
411
    cacheTTL?: number,
1✔
412
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1✔
413
    if (!fields) {
1!
414
      throw new Error("fields is empty");
×
415
    }
×
416
    return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
1✔
417
  }
1✔
418

419
  /**
420
   * Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
421
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
422
   *
423
   * @template TSelection - The type of the selected fields
424
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
425
   * @param {number} cacheTTL - cache ttl optional default is 60 sec.
426
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
427
   * @throws {Error} If fields parameter is empty
428
   * @example
429
   * ```typescript
430
   * await forgeSQL
431
   *   .selectDistinctCacheable({user: users, order: orders}, 60)
432
   *   .from(orders)
433
   *   .innerJoin(users, eq(orders.userId, users.id));
434
   * ```
435
   */
436
  selectDistinctCacheable<TSelection extends SelectedFields>(
6✔
437
    fields: TSelection,
1✔
438
    cacheTTL?: number,
1✔
439
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1✔
440
    if (!fields) {
1!
441
      throw new Error("fields is empty");
×
442
    }
×
443
    return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
1✔
444
  }
1✔
445

446
  /**
447
   * Creates a select query builder for all columns from a table with field aliasing support.
448
   * This is a convenience method that automatically selects all columns from the specified table.
449
   *
450
   * @template T - The type of the table
451
   * @param table - The table to select from
452
   * @returns Select query builder with all table columns and field aliasing support
453
   * @example
454
   * ```typescript
455
   * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
456
   * ```
457
   */
458
  selectFrom<T extends MySqlTable>(table: T) {
6✔
NEW
459
    return this.drizzle.selectFrom(table);
×
NEW
460
  }
×
461

462
  /**
463
   * Creates a select distinct query builder for all columns from a table with field aliasing support.
464
   * This is a convenience method that automatically selects all distinct columns from the specified table.
465
   *
466
   * @template T - The type of the table
467
   * @param table - The table to select from
468
   * @returns Select distinct query builder with all table columns and field aliasing support
469
   * @example
470
   * ```typescript
471
   * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
472
   * ```
473
   */
474
  selectDistinctFrom<T extends MySqlTable>(table: T) {
6✔
NEW
475
    return this.drizzle.selectDistinctFrom(table);
×
NEW
476
  }
×
477

478
  /**
479
   * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
480
   * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
481
   *
482
   * @template T - The type of the table
483
   * @param table - The table to select from
484
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
485
   * @returns Select query builder with all table columns, field aliasing, and caching support
486
   * @example
487
   * ```typescript
488
   * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
489
   * ```
490
   */
491
  selectCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
6✔
NEW
492
    return this.drizzle.selectFromCacheable(table, cacheTTL);
×
NEW
493
  }
×
494

495
  /**
496
   * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
497
   * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
498
   *
499
   * @template T - The type of the table
500
   * @param table - The table to select from
501
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
502
   * @returns Select distinct query builder with all table columns, field aliasing, and caching support
503
   * @example
504
   * ```typescript
505
   * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
506
   * ```
507
   */
508
  selectDistinctCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
6✔
NEW
509
    return this.drizzle.selectDistinctFromCacheable(table, cacheTTL);
×
NEW
510
  }
×
511

512
  /**
513
   * Executes a raw SQL query with local cache support.
514
   * This method provides local caching for raw SQL queries within the current invocation context.
515
   * Results are cached locally and will be returned from cache on subsequent identical queries.
516
   *
517
   * @param query - The SQL query to execute (SQLWrapper or string)
518
   * @returns Promise with query results
519
   * @example
520
   * ```typescript
521
   * // Using SQLWrapper
522
   * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
523
   * 
524
   * // Using string
525
   * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
526
   * ```
527
   */
528
  execute(query: SQLWrapper | string) {
6✔
NEW
529
    return this.drizzle.executeQuery(query);
×
NEW
530
  }
×
531

532
  /**
533
   * Executes a raw SQL query with both local and global cache support.
534
   * This method provides comprehensive caching for raw SQL queries:
535
   * - Local cache: Within the current invocation context
536
   * - Global cache: Cross-invocation caching using @forge/kvs
537
   *
538
   * @param query - The SQL query to execute (SQLWrapper or string)
539
   * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
540
   * @returns Promise with query results
541
   * @example
542
   * ```typescript
543
   * // Using SQLWrapper with custom TTL
544
   * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
545
   * 
546
   * // Using string with default TTL
547
   * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
548
   * ```
549
   */
550
  executeCacheable(query: SQLWrapper | string, cacheTtl?: number) {
6✔
NEW
551
    return this.drizzle.executeQueryCacheable(query, cacheTtl);
×
NEW
552
  }
×
553

554
  /**
555
   * Creates a Common Table Expression (CTE) builder for complex queries.
556
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
557
   *
558
   * @returns WithBuilder for creating CTEs
559
   * @example
560
   * ```typescript
561
   * const withQuery = forgeSQL.$with('userStats').as(
562
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
563
   *     .from(users)
564
   *     .groupBy(users.id)
565
   * );
566
   * ```
567
   */
568
  get $with() {
6✔
NEW
569
    return this.drizzle.$with;
×
NEW
570
  }
×
571

572
  /**
573
   * Creates a query builder that uses Common Table Expressions (CTEs).
574
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
575
   *
576
   * @param queries - Array of CTE queries created with $with()
577
   * @returns Query builder with CTE support
578
   * @example
579
   * ```typescript
580
   * const withQuery = forgeSQL.$with('userStats').as(
581
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
582
   *     .from(users)
583
   *     .groupBy(users.id)
584
   * );
585
   * 
586
   * const result = await forgeSQL.with(withQuery)
587
   *   .select({ userId: withQuery.userId, count: withQuery.count })
588
   *   .from(withQuery);
589
   * ```
590
   */
591
  with(...queries: WithSubquery[]) {
6✔
NEW
592
    return this.drizzle.with(...queries);
×
NEW
593
  }
×
594
}
6✔
595

596
/**
597
 * Public class that acts as a wrapper around the private ForgeSQLORMImpl.
598
 * Provides a clean interface for working with Forge SQL and Drizzle ORM.
599
 */
600
class ForgeSQLORM implements ForgeSqlOperation {
1✔
601
  private readonly ormInstance: ForgeSqlOperation;
77✔
602

603
  constructor(options?: ForgeSqlOrmOptions) {
77✔
604
    this.ormInstance = ForgeSQLORMImpl.getInstance(options);
77✔
605
  }
77✔
606

607
  selectCacheable<TSelection extends SelectedFields>(
77✔
608
    fields: TSelection,
1✔
609
    cacheTTL?: number,
1✔
610
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1✔
611
    return this.ormInstance.selectCacheable(fields, cacheTTL);
1✔
612
  }
1✔
613

614
  selectDistinctCacheable<TSelection extends SelectedFields>(
77✔
615
    fields: TSelection,
1✔
616
    cacheTTL?: number,
1✔
617
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1✔
618
    return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
1✔
619
  }
1✔
620

621
  /**
622
   * Creates a select query builder for all columns from a table with field aliasing support.
623
   * This is a convenience method that automatically selects all columns from the specified table.
624
   *
625
   * @template T - The type of the table
626
   * @param table - The table to select from
627
   * @returns Select query builder with all table columns and field aliasing support
628
   * @example
629
   * ```typescript
630
   * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
631
   * ```
632
   */
633
  selectFrom<T extends MySqlTable>(table: T) {
77✔
634
    return this.ormInstance.getDrizzleQueryBuilder().selectFrom(table);
1✔
635
  }
1✔
636

637
  /**
638
   * Creates a select distinct query builder for all columns from a table with field aliasing support.
639
   * This is a convenience method that automatically selects all distinct columns from the specified table.
640
   *
641
   * @template T - The type of the table
642
   * @param table - The table to select from
643
   * @returns Select distinct query builder with all table columns and field aliasing support
644
   * @example
645
   * ```typescript
646
   * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
647
   * ```
648
   */
649
  selectDistinctFrom<T extends MySqlTable>(table: T) {
77✔
650
    return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFrom(table);
1✔
651
  }
1✔
652

653
  /**
654
   * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
655
   * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
656
   *
657
   * @template T - The type of the table
658
   * @param table - The table to select from
659
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
660
   * @returns Select query builder with all table columns, field aliasing, and caching support
661
   * @example
662
   * ```typescript
663
   * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
664
   * ```
665
   */
666
  selectCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
77✔
667
    return this.ormInstance.getDrizzleQueryBuilder().selectFromCacheable(table, cacheTTL);
1✔
668
  }
1✔
669

670
  /**
671
   * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
672
   * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
673
   *
674
   * @template T - The type of the table
675
   * @param table - The table to select from
676
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
677
   * @returns Select distinct query builder with all table columns, field aliasing, and caching support
678
   * @example
679
   * ```typescript
680
   * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
681
   * ```
682
   */
683
  selectDistinctCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
77✔
684
    return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFromCacheable(table, cacheTTL);
1✔
685
  }
1✔
686

687
  executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void> {
77✔
688
    return this.ormInstance.executeWithCacheContext(cacheContext);
6✔
689
  }
6✔
690
  executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
77✔
691
    return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
×
692
  }
×
693
  /**
694
   * Executes operations within a local cache context.
695
   * This provides in-memory caching for select queries within a single request scope.
696
   * 
697
   * @param cacheContext - Function containing operations that will benefit from local caching
698
   * @returns Promise that resolves when all operations are complete
699
   */
700
  executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void> {
77✔
701
    return this.ormInstance.executeWithLocalContext(cacheContext);
9✔
702
  }
9✔
703

704
  /**
705
   * Executes operations within a local cache context and returns a value.
706
   * This provides in-memory caching for select queries within a single request scope.
707
   * 
708
   * @param cacheContext - Function containing operations that will benefit from local caching
709
   * @returns Promise that resolves to the return value of the cacheContext function
710
   */
711
  executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
77✔
712
    return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
3✔
713
  }
3✔
714

715
  /**
716
   * Creates an insert query builder.
717
   *
718
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
719
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
720
   *
721
   * @param table - The table to insert into
722
   * @returns Insert query builder (no versioning, no cache management)
723
   */
724
  insert<TTable extends MySqlTable>(
77✔
725
    table: TTable,
5✔
726
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
5✔
727
    return this.ormInstance.insert(table);
5✔
728
  }
5✔
729

730
  /**
731
   * Creates an insert query builder that automatically evicts cache after execution.
732
   *
733
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
734
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
735
   *
736
   * @param table - The table to insert into
737
   * @returns Insert query builder with automatic cache eviction (no versioning)
738
   */
739
  insertAndEvictCache<TTable extends MySqlTable>(
77✔
740
    table: TTable,
2✔
741
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
2✔
742
    return this.ormInstance.insertAndEvictCache(table);
2✔
743
  }
2✔
744

745
  /**
746
   * Creates an update query builder.
747
   *
748
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
749
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
750
   *
751
   * @param table - The table to update
752
   * @returns Update query builder (no versioning, no cache management)
753
   */
754
  update<TTable extends MySqlTable>(
77✔
755
    table: TTable,
3✔
756
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
3✔
757
    return this.ormInstance.update(table);
3✔
758
  }
3✔
759

760
  /**
761
   * Creates an update query builder that automatically evicts cache after execution.
762
   *
763
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
764
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
765
   *
766
   * @param table - The table to update
767
   * @returns Update query builder with automatic cache eviction (no versioning)
768
   */
769
  updateAndEvictCache<TTable extends MySqlTable>(
77✔
770
    table: TTable,
2✔
771
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
2✔
772
    return this.ormInstance.updateAndEvictCache(table);
2✔
773
  }
2✔
774

775
  /**
776
   * Creates a delete query builder.
777
   *
778
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
779
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
780
   *
781
   * @param table - The table to delete from
782
   * @returns Delete query builder (no versioning, no cache management)
783
   */
784
  delete<TTable extends MySqlTable>(
77✔
785
    table: TTable,
3✔
786
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
3✔
787
    return this.ormInstance.delete(table);
3✔
788
  }
3✔
789

790
  /**
791
   * Creates a delete query builder that automatically evicts cache after execution.
792
   *
793
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
794
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
795
   *
796
   * @param table - The table to delete from
797
   * @returns Delete query builder with automatic cache eviction (no versioning)
798
   */
799
  deleteAndEvictCache<TTable extends MySqlTable>(
77✔
800
    table: TTable,
2✔
801
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
2✔
802
    return this.ormInstance.deleteAndEvictCache(table);
2✔
803
  }
2✔
804

805
  /**
806
   * Creates a select query with unique field aliases to prevent field name collisions in joins.
807
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
808
   *
809
   * @template TSelection - The type of the selected fields
810
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
811
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
812
   * @throws {Error} If fields parameter is empty
813
   * @example
814
   * ```typescript
815
   * await forgeSQL
816
   *   .select({user: users, order: orders})
817
   *   .from(orders)
818
   *   .innerJoin(users, eq(orders.userId, users.id));
819
   * ```
820
   */
821
  select<TSelection extends SelectedFields>(
77✔
822
    fields: TSelection,
19✔
823
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
19✔
824
    return this.ormInstance.select(fields);
19✔
825
  }
19✔
826

827
  /**
828
   * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
829
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
830
   *
831
   * @template TSelection - The type of the selected fields
832
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
833
   * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases
834
   * @throws {Error} If fields parameter is empty
835
   * @example
836
   * ```typescript
837
   * await forgeSQL
838
   *   .selectDistinct({user: users, order: orders})
839
   *   .from(orders)
840
   *   .innerJoin(users, eq(orders.userId, users.id));
841
   * ```
842
   */
843
  selectDistinct<TSelection extends SelectedFields>(
77✔
844
    fields: TSelection,
1✔
845
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
1✔
846
    return this.ormInstance.selectDistinct(fields);
1✔
847
  }
1✔
848

849
  /**
850
   * Proxies the `modify` method from `ForgeSQLORMImpl`.
851
   * @returns Modify operations.
852
   */
853
  modifyWithVersioning(): VerioningModificationForgeSQL {
77✔
854
    return this.ormInstance.modifyWithVersioning();
25✔
855
  }
25✔
856

857
  /**
858
   * Proxies the `fetch` method from `ForgeSQLORMImpl`.
859
   * @returns Fetch operations.
860
   */
861
  fetch(): SchemaSqlForgeSql {
77✔
862
    return this.ormInstance.fetch();
5✔
863
  }
5✔
864

865
  /**
866
   * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
867
   * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
868
   */
869
  analyze(): SchemaAnalyzeForgeSql {
77✔
870
    return this.ormInstance.analyze();
1✔
871
  }
1✔
872

873
  /**
874
   * Provides schema-level SQL cacheable operations with type safety.
875
   * @returns {ForgeSQLCacheOperations} Interface for executing schema-bound SQL queries
876
   */
877
  modifyWithVersioningAndEvictCache(): ForgeSQLCacheOperations {
77✔
878
    return this.ormInstance.modifyWithVersioningAndEvictCache();
3✔
879
  }
3✔
880

881
  /**
882
   * Returns a Drizzle query builder instance.
883
   *
884
   * @returns A Drizzle query builder instance for query construction only.
885
   */
886
  getDrizzleQueryBuilder() {
77✔
887
    return this.ormInstance.getDrizzleQueryBuilder();
8✔
888
  }
8✔
889

890
  /**
891
   * Executes a raw SQL query with local cache support.
892
   * This method provides local caching for raw SQL queries within the current invocation context.
893
   * Results are cached locally and will be returned from cache on subsequent identical queries.
894
   *
895
   * @param query - The SQL query to execute (SQLWrapper or string)
896
   * @returns Promise with query results
897
   * @example
898
   * ```typescript
899
   * // Using SQLWrapper
900
   * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
901
   * 
902
   * // Using string
903
   * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
904
   * ```
905
   */
906
  execute(query: SQLWrapper | string) {
77✔
907
    return this.ormInstance.getDrizzleQueryBuilder().executeQuery(query);
1✔
908
  }
1✔
909

910
  /**
911
   * Executes a raw SQL query with both local and global cache support.
912
   * This method provides comprehensive caching for raw SQL queries:
913
   * - Local cache: Within the current invocation context
914
   * - Global cache: Cross-invocation caching using @forge/kvs
915
   *
916
   * @param query - The SQL query to execute (SQLWrapper or string)
917
   * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
918
   * @returns Promise with query results
919
   * @example
920
   * ```typescript
921
   * // Using SQLWrapper with custom TTL
922
   * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
923
   * 
924
   * // Using string with default TTL
925
   * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
926
   * ```
927
   */
928
  executeCacheable(query: SQLWrapper | string, cacheTtl?: number) {
77✔
929
    return this.ormInstance.getDrizzleQueryBuilder().executeQueryCacheable(query, cacheTtl);
1✔
930
  }
1✔
931

932
  /**
933
   * Creates a Common Table Expression (CTE) builder for complex queries.
934
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
935
   *
936
   * @returns WithBuilder for creating CTEs
937
   * @example
938
   * ```typescript
939
   * const withQuery = forgeSQL.$with('userStats').as(
940
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
941
   *     .from(users)
942
   *     .groupBy(users.id)
943
   * );
944
   * ```
945
   */
946
  get $with() {
77✔
947
    return this.ormInstance.getDrizzleQueryBuilder().$with;
1✔
948
  }
1✔
949

950
  /**
951
   * Creates a query builder that uses Common Table Expressions (CTEs).
952
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
953
   *
954
   * @param queries - Array of CTE queries created with $with()
955
   * @returns Query builder with CTE support
956
   * @example
957
   * ```typescript
958
   * const withQuery = forgeSQL.$with('userStats').as(
959
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
960
   *     .from(users)
961
   *     .groupBy(users.id)
962
   * );
963
   * 
964
   * const result = await forgeSQL.with(withQuery)
965
   *   .select({ userId: withQuery.userId, count: withQuery.count })
966
   *   .from(withQuery);
967
   * ```
968
   */
969
  with(...queries: WithSubquery[]) {
77✔
970
    return this.ormInstance.getDrizzleQueryBuilder().with(...queries);
1✔
971
  }
1✔
972
}
77✔
973

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

© 2026 Coveralls, Inc