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

vzakharchenko / forge-sql-orm / 18728066491

22 Oct 2025 07:50PM UTC coverage: 82.832% (+0.08%) from 82.757%
18728066491

push

github

vzakharchenko
fix tests

518 of 638 branches covered (81.19%)

Branch coverage included in aggregate %.

2536 of 3049 relevant lines covered (83.17%)

13.22 hits per line

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

72.41
/src/core/ForgeSQLQueryBuilder.ts
1
import { UpdateQueryResponse } from "@forge/sql";
2
import { SqlParameters } from "@forge/sql/out/sql-statement";
3
import {
1✔
4
  AnyMySqlSelectQueryBuilder,
5
  AnyMySqlTable,
6
  customType,
7
  MySqlSelectBuilder,
8
  MySqlTable,
9
} from "drizzle-orm/mysql-core";
10
import {
11
  MySqlSelectDynamic,
12
  type SelectedFields,
13
} from "drizzle-orm/mysql-core/query-builders/select.types";
14
import { InferInsertModel, Query, SQL } from "drizzle-orm";
15
import { parseDateTime, formatDateTime } from "../utils/sqlUtils";
1✔
16
import {
17
  MySqlRemoteDatabase,
18
  MySqlRemotePreparedQueryHKT,
19
  MySqlRemoteQueryResultHKT,
20
} from "drizzle-orm/mysql-proxy";
21
import { SqlHints } from "../utils/sqlHints";
22
import {
23
  ClusterStatementRowCamelCase,
24
  ExplainAnalyzeRow,
25
  SlowQueryNormalized,
26
} from "./SystemTables";
27
import { ForgeSQLCacheOperations } from "./ForgeSQLCacheOperations";
28
import {
29
  DeleteAndEvictCacheType,
30
  ExecuteQuery,
31
  ExecuteQueryCacheable,
32
  ForgeSQLMetadata,
33
  InsertAndEvictCacheType,
34
  SelectAliasedCacheableType,
35
  SelectAliasedDistinctCacheableType,
36
  SelectAliasedDistinctType,
37
  SelectAliasedType,
38
  SelectAllDistinctFromAliasedType,
39
  SelectAllDistinctFromCacheableAliasedType,
40
  SelectAllFromAliasedType,
41
  SelectAllFromCacheableAliasedType,
42
  UpdateAndEvictCacheType,
43
} from "..";
44
import {
45
  MySqlDeleteBase,
46
  MySqlInsertBuilder,
47
  MySqlSelectBase,
48
  MySqlUpdateBuilder,
49
} from "drizzle-orm/mysql-core/query-builders";
50

51
import {
52
  GetSelectTableName,
53
  GetSelectTableSelection,
54
  SelectResultField,
55
} from "drizzle-orm/query-builders/select.types";
56
import { SQLWrapper } from "drizzle-orm/sql/sql";
57
import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
58
import type { WithBuilder } from "drizzle-orm/mysql-core/subquery";
59
import { WithSubquery } from "drizzle-orm/subquery";
60

61
/**
62
 * Core interface for ForgeSQL operations.
63
 * Provides access to CRUD operations, schema-level SQL operations, and query analysis capabilities.
64
 *
65
 * This is the main interface that developers interact with when using ForgeSQL ORM.
66
 * It combines query building capabilities with database operations and caching.
67
 *
68
 * @interface ForgeSqlOperation
69
 * @extends {QueryBuilderForgeSql}
70
 */
71
export interface ForgeSqlOperation extends QueryBuilderForgeSql {
72
  /**
73
   * Creates a new query builder for the given entity.
74
   * @returns {MySqlRemoteDatabase<Record<string, unknown>>} The Drizzle database instance for building queries
75
   */
76
  getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
77
    selectAliased: SelectAliasedType;
78
    selectAliasedDistinct: SelectAliasedDistinctType;
79
    executeQuery: ExecuteQuery;
80
    selectAliasedCacheable: SelectAliasedCacheableType;
81
    selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
82
    executeQueryCacheable: ExecuteQueryCacheable;
83
    insertWithCacheContext: InsertAndEvictCacheType;
84
    insertAndEvictCache: InsertAndEvictCacheType;
85
    updateAndEvictCache: UpdateAndEvictCacheType;
86
    updateWithCacheContext: UpdateAndEvictCacheType;
87
    deleteAndEvictCache: DeleteAndEvictCacheType;
88
    deleteWithCacheContext: DeleteAndEvictCacheType;
89
    selectFrom: SelectAllFromAliasedType;
90
    selectDistinctFrom: SelectAllDistinctFromAliasedType;
91
    selectFromCacheable: SelectAllFromCacheableAliasedType;
92
    selectDistinctFromCacheable: SelectAllDistinctFromCacheableAliasedType;
93
  };
94

95
  /**
96
   * Provides modify (Create, Update, Delete) operations with optimistic locking support.
97
   * @returns {VerioningModificationForgeSQL} Interface for performing CRUD operations
98
   */
99
  modifyWithVersioning(): VerioningModificationForgeSQL;
100

101
  /**
102
   * Provides schema-level SQL fetch operations with type safety.
103
   * @returns {SchemaSqlForgeSql} Interface for executing schema-bound SQL queries
104
   */
105
  fetch(): SchemaSqlForgeSql;
106

107
  /**
108
   * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
109
   * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
110
   */
111
  analyze(): SchemaAnalyzeForgeSql;
112

113
  /**
114
   * Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
115
   *
116
   * This method returns operations that use `modifyWithVersioning()` internally, providing:
117
   * - Optimistic locking support
118
   * - Automatic version field management
119
   * - Cache eviction after successful operations
120
   *
121
   * @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
122
   */
123
  modifyWithVersioningAndEvictCache(): ForgeSQLCacheOperations;
124
}
125

126
/**
127
 * Interface for Query Builder operations.
128
 * Provides access to the underlying Drizzle ORM query builder with enhanced functionality.
129
 *
130
 * This interface extends Drizzle's query building capabilities with:
131
 * - Field aliasing to prevent name collisions in joins
132
 * - Caching support for select operations
133
 * - Automatic cache eviction for modify operations
134
 *
135
 * @interface QueryBuilderForgeSql
136
 */
137
export interface QueryBuilderForgeSql {
138
  /**
139
   * Creates a new query builder for the given entity.
140
   * @returns {MySqlRemoteDatabase<Record<string, unknown>>} The Drizzle database instance for building queries
141
   */
142
  getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
143
    selectAliased: SelectAliasedType;
144
    selectAliasedDistinct: SelectAliasedDistinctType;
145
    executeQuery: ExecuteQuery;
146
    selectAliasedCacheable: SelectAliasedCacheableType;
147
    selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
148
    executeQueryCacheable: ExecuteQueryCacheable;
149
    insertWithCacheContext: InsertAndEvictCacheType;
150
    insertAndEvictCache: InsertAndEvictCacheType;
151
    updateAndEvictCache: UpdateAndEvictCacheType;
152
    updateWithCacheContext: UpdateAndEvictCacheType;
153
    deleteAndEvictCache: DeleteAndEvictCacheType;
154
    deleteWithCacheContext: DeleteAndEvictCacheType;
155
  };
156

157
  /**
158
   * Creates a select query with unique field aliases to prevent field name collisions in joins.
159
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
160
   *
161
   * @template TSelection - The type of the selected fields
162
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
163
   * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A select query builder with unique field aliases
164
   * @throws {Error} If fields parameter is empty
165
   * @example
166
   * ```typescript
167
   * await forgeSQL
168
   *   .select({user: users, order: orders})
169
   *   .from(orders)
170
   *   .innerJoin(users, eq(orders.userId, users.id));
171
   * ```
172
   */
173
  select<TSelection extends SelectedFields>(
174
    fields: TSelection,
175
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
176

177
  /**
178
   * Creates a select query builder for all columns from a table with field aliasing support.
179
   * This is a convenience method that automatically selects all columns from the specified table.
180
   *
181
   * @template T - The type of the table
182
   * @param table - The table to select from
183
   * @returns Select query builder with all table columns and field aliasing support
184
   * @example
185
   * ```typescript
186
   * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
187
   * ```
188
   */
189
  selectFrom<T extends MySqlTable>(
190
    table: T,
191
  ): MySqlSelectBase<
192
    GetSelectTableName<T>,
193
    GetSelectTableSelection<T>,
194
    "single",
195
    MySqlRemotePreparedQueryHKT,
196
    GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
197
    false,
198
    never,
199
    {
200
      [K in keyof {
201
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
202
          GetSelectTableSelection<T>[Key]
203
        >;
204
      }]: {
205
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
206
          GetSelectTableSelection<T>[Key]
207
        >;
208
      }[K];
209
    }[],
210
    any
211
  >;
212

213
  /**
214
   * Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
215
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
216
   *
217
   * @template TSelection - The type of the selected fields
218
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
219
   * @returns {MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>} A distinct select query builder with unique field aliases
220
   * @throws {Error} If fields parameter is empty
221
   * @example
222
   * ```typescript
223
   * await forgeSQL
224
   *   .selectDistinct({user: users, order: orders})
225
   *   .from(orders)
226
   *   .innerJoin(users, eq(orders.userId, users.id));
227
   * ```
228
   */
229
  selectDistinct<TSelection extends SelectedFields>(
230
    fields: TSelection,
231
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
232
  /**
233
   * Creates a select distinct query builder for all columns from a table with field aliasing support.
234
   * This is a convenience method that automatically selects all distinct columns from the specified table.
235
   *
236
   * @template T - The type of the table
237
   * @param table - The table to select from
238
   * @returns Select distinct query builder with all table columns and field aliasing support
239
   * @example
240
   * ```typescript
241
   * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
242
   * ```
243
   */
244
  selectDistinctFrom<T extends MySqlTable>(
245
    table: T,
246
  ): MySqlSelectBase<
247
    GetSelectTableName<T>,
248
    GetSelectTableSelection<T>,
249
    "single",
250
    MySqlRemotePreparedQueryHKT,
251
    GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
252
    false,
253
    never,
254
    {
255
      [K in keyof {
256
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
257
          GetSelectTableSelection<T>[Key]
258
        >;
259
      }]: {
260
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
261
          GetSelectTableSelection<T>[Key]
262
        >;
263
      }[K];
264
    }[],
265
    any
266
  >;
267

268
  /**
269
   * Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
270
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
271
   *
272
   * @template TSelection - The type of the selected fields
273
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
274
   * @param {number} cacheTTL - cache ttl optional default is 60 sec.
275
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
276
   * @throws {Error} If fields parameter is empty
277
   * @example
278
   * ```typescript
279
   * await forgeSQL
280
   *   .selectCacheable({user: users, order: orders},60)
281
   *   .from(orders)
282
   *   .innerJoin(users, eq(orders.userId, users.id));
283
   * ```
284
   */
285
  selectCacheable<TSelection extends SelectedFields>(
286
    fields: TSelection,
287
    cacheTTL?: number,
288
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
289

290
  /**
291
   * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
292
   * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
293
   *
294
   * @template T - The type of the table
295
   * @param table - The table to select from
296
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
297
   * @returns Select query builder with all table columns, field aliasing, and caching support
298
   * @example
299
   * ```typescript
300
   * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
301
   * ```
302
   */
303
  selectCacheableFrom<T extends MySqlTable>(
304
    table: T,
305
    cacheTTL?: number,
306
  ): MySqlSelectBase<
307
    GetSelectTableName<T>,
308
    GetSelectTableSelection<T>,
309
    "single",
310
    MySqlRemotePreparedQueryHKT,
311
    GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
312
    false,
313
    never,
314
    {
315
      [K in keyof {
316
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
317
          GetSelectTableSelection<T>[Key]
318
        >;
319
      }]: {
320
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
321
          GetSelectTableSelection<T>[Key]
322
        >;
323
      }[K];
324
    }[],
325
    any
326
  >;
327

328
  /**
329
   * Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
330
   * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
331
   *
332
   * @template TSelection - The type of the selected fields
333
   * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
334
   * @param {number} cacheTTL - cache ttl optional default is 60 sec.
335
   * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
336
   * @throws {Error} If fields parameter is empty
337
   * @example
338
   * ```typescript
339
   * await forgeSQL
340
   *   .selectDistinctCacheable({user: users, order: orders}, 60)
341
   *   .from(orders)
342
   *   .innerJoin(users, eq(orders.userId, users.id));
343
   * ```
344
   */
345
  selectDistinctCacheable<TSelection extends SelectedFields>(
346
    fields: TSelection,
347
    cacheTTL?: number,
348
  ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
349

350
  /**
351
   * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
352
   * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
353
   *
354
   * @template T - The type of the table
355
   * @param table - The table to select from
356
   * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
357
   * @returns Select distinct query builder with all table columns, field aliasing, and caching support
358
   * @example
359
   * ```typescript
360
   * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
361
   * ```
362
   */
363
  selectDistinctCacheableFrom<T extends MySqlTable>(
364
    table: T,
365
    cacheTTL?: number,
366
  ): MySqlSelectBase<
367
    GetSelectTableName<T>,
368
    GetSelectTableSelection<T>,
369
    "single",
370
    MySqlRemotePreparedQueryHKT,
371
    GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {},
372
    false,
373
    never,
374
    {
375
      [K in keyof {
376
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
377
          GetSelectTableSelection<T>[Key]
378
        >;
379
      }]: {
380
        [Key in keyof GetSelectTableSelection<T>]: SelectResultField<
381
          GetSelectTableSelection<T>[Key]
382
        >;
383
      }[K];
384
    }[],
385
    any
386
  >;
387

388
  /**
389
   * Creates an insert query builder.
390
   *
391
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
392
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
393
   *
394
   * @param table - The table to insert into
395
   * @returns Insert query builder (no versioning, no cache management)
396
   */
397
  insert<TTable extends MySqlTable>(
398
    table: TTable,
399
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
400

401
  /**
402
   * Creates an insert query builder that automatically evicts cache after execution.
403
   *
404
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
405
   * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
406
   *
407
   * @param table - The table to insert into
408
   * @returns Insert query builder with automatic cache eviction (no versioning)
409
   */
410
  insertAndEvictCache<TTable extends MySqlTable>(
411
    table: TTable,
412
  ): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
413

414
  /**
415
   * Creates an update query builder.
416
   *
417
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
418
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
419
   *
420
   * @param table - The table to update
421
   * @returns Update query builder (no versioning, no cache management)
422
   */
423
  update<TTable extends MySqlTable>(
424
    table: TTable,
425
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
426

427
  /**
428
   * Creates an update query builder that automatically evicts cache after execution.
429
   *
430
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
431
   * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
432
   *
433
   * @param table - The table to update
434
   * @returns Update query builder with automatic cache eviction (no versioning)
435
   */
436
  updateAndEvictCache<TTable extends MySqlTable>(
437
    table: TTable,
438
  ): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
439

440
  /**
441
   * Creates a delete query builder.
442
   *
443
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
444
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
445
   *
446
   * @param table - The table to delete from
447
   * @returns Delete query builder (no versioning, no cache management)
448
   */
449
  delete<TTable extends MySqlTable>(
450
    table: TTable,
451
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
452
  /**
453
   * Creates a delete query builder that automatically evicts cache after execution.
454
   *
455
   * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
456
   * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
457
   *
458
   * @param table - The table to delete from
459
   * @returns Delete query builder with automatic cache eviction (no versioning)
460
   */
461
  deleteAndEvictCache<TTable extends MySqlTable>(
462
    table: TTable,
463
  ): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT>;
464

465
  /**
466
   * Executes operations within a cache context that collects cache eviction events.
467
   * All clearCache calls within the context are collected and executed in batch at the end.
468
   * Queries executed within this context will bypass cache for tables that were marked for clearing.
469
   *
470
   * @param cacheContext - Function containing operations that may trigger cache evictions
471
   * @returns Promise that resolves when all operations and cache clearing are complete
472
   */
473
  executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void>;
474

475
  /**
476
   * Executes operations within a cache context and returns a value.
477
   * All clearCache calls within the context are collected and executed in batch at the end.
478
   * Queries executed within this context will bypass cache for tables that were marked for clearing.
479
   *
480
   * @param cacheContext - Function containing operations that may trigger cache evictions
481
   * @returns Promise that resolves to the return value of the cacheContext function
482
   */
483
  executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T>;
484

485
  /**
486
   * Executes operations within a local cache context that provides in-memory caching for select queries.
487
   * This is useful for optimizing queries within a single resolver or request scope.
488
   *
489
   * Local cache features:
490
   * - Caches select query results in memory for the duration of the context
491
   * - Automatically evicts cache when insert/update/delete operations are performed
492
   * - Provides faster access to repeated queries within the same context
493
   * - Does not persist across different requests or contexts
494
   *
495
   * @param cacheContext - Function containing operations that will benefit from local caching
496
   * @returns Promise that resolves when all operations are complete
497
   *
498
   * @example
499
   * ```typescript
500
   * await forgeSQL.executeWithLocalContext(async () => {
501
   *   // First call - executes query and caches result
502
   *   const users = await forgeSQL.select({ id: users.id, name: users.name })
503
   *     .from(users).where(eq(users.active, true));
504
   *
505
   *   // Second call - gets result from local cache (no database query)
506
   *   const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
507
   *     .from(users).where(eq(users.active, true));
508
   *
509
   *   // Insert operation - evicts local cache
510
   *   await forgeSQL.insert(users).values({ name: 'New User', active: true });
511
   * });
512
   * ```
513
   */
514
  executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void>;
515

516
  /**
517
   * Executes operations within a local cache context and returns a value.
518
   * This is useful for optimizing queries within a single resolver or request scope.
519
   *
520
   * Local cache features:
521
   * - Caches select query results in memory for the duration of the context
522
   * - Automatically evicts cache when insert/update/delete operations are performed
523
   * - Provides faster access to repeated queries within the same context
524
   * - Does not persist across different requests or contexts
525
   *
526
   * @param cacheContext - Function containing operations that will benefit from local caching
527
   * @returns Promise that resolves to the return value of the cacheContext function
528
   *
529
   * @example
530
   * ```typescript
531
   * const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
532
   *   // First call - executes query and caches result
533
   *   const users = await forgeSQL.select({ id: users.id, name: users.name })
534
   *     .from(users).where(eq(users.active, true));
535
   *
536
   *   // Second call - gets result from local cache (no database query)
537
   * const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
538
   *     .from(users).where(eq(users.active, true));
539
   *
540
   *   return { users, cachedUsers };
541
   * });
542
   * ```
543
   */
544
  executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T>;
545

546
  /**
547
   * Executes a query and provides access to execution metadata.
548
   * This method allows you to capture detailed information about query execution
549
   * including database execution time, response size, and Forge SQL metadata.
550
   *
551
   * @template T - The return type of the query
552
   * @param query - A function that returns a Promise with the query result
553
   * @param onMetadata - Callback function that receives execution metadata
554
   * @returns Promise with the query result
555
   * @example
556
   * ```typescript
557
   * const result = await forgeSQL.executeWithMetadata(
558
   *   async () => await forgeSQL.select().from(users).where(eq(users.id, 1)),
559
   *   (dbTime, responseSize, metadata) => {
560
   *     console.log(`DB execution time: ${dbTime}ms`);
561
   *     console.log(`Response size: ${responseSize} bytes`);
562
   *     console.log('Forge metadata:', metadata);
563
   *   }
564
   * );
565
   * ```
566
   */
567
  executeWithMetadata<T>(
568
    query: () => Promise<T>,
569
    onMetadata: (
570
      totalDbExecutionTime: number,
571
      totalResponseSize: number,
572
      forgeMetadata: ForgeSQLMetadata,
573
    ) => Promise<void> | void,
574
  ): Promise<T>;
575
  /**
576
   * Executes a raw SQL query with local cache support.
577
   * This method provides local caching for raw SQL queries within the current invocation context.
578
   * Results are cached locally and will be returned from cache on subsequent identical queries.
579
   *
580
   * @param query - The SQL query to execute (SQLWrapper or string)
581
   * @returns Promise with query results
582
   * @example
583
   * ```typescript
584
   * // Using SQLWrapper
585
   * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
586
   *
587
   * // Using string
588
   * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
589
   * ```
590
   */
591
  execute<T>(
592
    query: SQLWrapper | string,
593
  ): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>>;
594

595
  /**
596
   * Executes a Data Definition Language (DDL) SQL query.
597
   * DDL operations include CREATE, ALTER, DROP, TRUNCATE, and other schema modification statements.
598
   *
599
   * This method is specifically designed for DDL operations and provides:
600
   * - Proper operation type context for DDL queries
601
   * - No caching (DDL operations should not be cached)
602
   * - Direct execution without query optimization
603
   *
604
   * @template T - The expected return type of the query result
605
   * @param query - The DDL SQL query to execute (SQLWrapper or string)
606
   * @returns Promise with query results
607
   * @throws {Error} If the DDL operation fails
608
   *
609
   * @example
610
   * ```typescript
611
   * // Create a new table
612
   * await forgeSQL.executeDDL(`
613
   *   CREATE TABLE users (
614
   *     id INT PRIMARY KEY AUTO_INCREMENT,
615
   *     name VARCHAR(255) NOT NULL,
616
   *     email VARCHAR(255) UNIQUE
617
   *   )
618
   * `);
619
   *
620
   * // Alter table structure
621
   * await forgeSQL.executeDDL(sql`
622
   *   ALTER TABLE users
623
   *   ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
624
   * `);
625
   *
626
   * // Drop a table
627
   * await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
628
   * ```
629
   */
630
  executeDDL<T>(
631
    query: SQLWrapper | string,
632
  ): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>>;
633

634
  /**
635
   * Executes a series of actions within a DDL operation context.
636
   * This method provides a way to execute regular SQL queries that should be treated
637
   * as DDL operations, ensuring proper operation type context for performance monitoring.
638
   *
639
   * This method is useful for:
640
   * - Executing regular SQL queries in DDL context for monitoring purposes
641
   * - Wrapping non-DDL operations that should be treated as DDL for analysis
642
   * - Ensuring proper operation type context for complex workflows
643
   * - Maintaining DDL operation context across multiple function calls
644
   *
645
   * @template T - The return type of the actions function
646
   * @param actions - Function containing SQL operations to execute in DDL context
647
   * @returns Promise that resolves to the return value of the actions function
648
   *
649
   * @example
650
   * ```typescript
651
   * // Execute regular SQL queries in DDL context for monitoring
652
   * await forgeSQL.executeDDLActions(async () => {
653
   *   const slowQueries = await forgeSQL.execute(`
654
   *     SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
655
   *     WHERE AVG_LATENCY > 1000000
656
   *   `);
657
   *   return slowQueries;
658
   * });
659
   *
660
   * // Execute complex analysis queries in DDL context
661
   * const result = await forgeSQL.executeDDLActions(async () => {
662
   *   const tableInfo = await forgeSQL.execute("SHOW TABLES");
663
   *   const performanceData = await forgeSQL.execute(`
664
   *     SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
665
   *     WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
666
   *   `);
667
   *   return { tableInfo, performanceData };
668
   * });
669
   *
670
   * // Execute monitoring queries with error handling
671
   * try {
672
   *   await forgeSQL.executeDDLActions(async () => {
673
   *     const metrics = await forgeSQL.execute(`
674
   *       SELECT COUNT(*) as query_count
675
   *       FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
676
   *     `);
677
   *     console.log(`Total queries: ${metrics[0].query_count}`);
678
   *   });
679
   * } catch (error) {
680
   *   console.error("Monitoring query failed:", error);
681
   * }
682
   * ```
683
   */
684
  executeDDLActions<T>(actions: () => Promise<T>): Promise<T>;
685

686
  /**
687
   * Executes a raw SQL query with both local and global cache support.
688
   * This method provides comprehensive caching for raw SQL queries:
689
   * - Local cache: Within the current invocation context
690
   * - Global cache: Cross-invocation caching using @forge/kvs
691
   *
692
   * @param query - The SQL query to execute (SQLWrapper or string)
693
   * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
694
   * @returns Promise with query results
695
   * @example
696
   * ```typescript
697
   * // Using SQLWrapper with custom TTL
698
   * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
699
   *
700
   * // Using string with default TTL
701
   * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
702
   * ```
703
   */
704
  executeCacheable<T>(
705
    query: SQLWrapper | string,
706
    cacheTtl?: number,
707
  ): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>>;
708
  /**
709
   * Creates a Common Table Expression (CTE) builder for complex queries.
710
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
711
   *
712
   * @returns WithBuilder for creating CTEs
713
   * @example
714
   * ```typescript
715
   * const withQuery = forgeSQL.$with('userStats').as(
716
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
717
   *     .from(users)
718
   *     .groupBy(users.id)
719
   * );
720
   * ```
721
   */
722
  $with: WithBuilder;
723

724
  /**
725
   * Creates a query builder that uses Common Table Expressions (CTEs).
726
   * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
727
   *
728
   * @param queries - Array of CTE queries created with $with()
729
   * @returns Query builder with CTE support
730
   * @example
731
   * ```typescript
732
   * const withQuery = forgeSQL.$with('userStats').as(
733
   *   forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
734
   *     .from(users)
735
   *     .groupBy(users.id)
736
   * );
737
   *
738
   * const result = await forgeSQL.with(withQuery)
739
   *   .select({ userId: withQuery.userId, count: withQuery.count })
740
   *   .from(withQuery);
741
   * ```
742
   */
743
  with(...queries: WithSubquery[]): {
744
    select: {
745
      (): MySqlSelectBuilder<undefined, MySqlRemotePreparedQueryHKT>;
746
      <TSelection extends SelectedFields>(
747
        fields: TSelection,
748
      ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
749
    };
750
    selectDistinct: {
751
      (): MySqlSelectBuilder<undefined, MySqlRemotePreparedQueryHKT>;
752
      <TSelection extends SelectedFields>(
753
        fields: TSelection,
754
      ): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
755
    };
756
  };
757
}
758

759
/**
760
 * Interface for Modify (Create, Update, Delete) operations.
761
 * Provides methods for basic database operations with support for optimistic locking.
762
 *
763
 * @interface VerioningModificationForgeSQL
764
 */
765
export interface VerioningModificationForgeSQL {
766
  /**
767
   * Inserts multiple records into the database.
768
   * @template T - The type of the table schema
769
   * @param {T} schema - The entity schema
770
   * @param {InferInsertModel<T>[]} models - The list of entities to insert
771
   * @param {boolean} [updateIfExists] - Whether to update the row if it already exists (default: false)
772
   * @returns {Promise<number>} The number of inserted rows
773
   * @throws {Error} If the insert operation fails
774
   */
775
  insert<T extends AnyMySqlTable>(
776
    schema: T,
777
    models: InferInsertModel<T>[],
778
    updateIfExists?: boolean,
779
  ): Promise<number>;
780

781
  /**
782
   * Deletes a record by its ID.
783
   * @template T - The type of the table schema
784
   * @param {unknown} id - The ID of the record to delete
785
   * @param {T} schema - The entity schema
786
   * @returns {Promise<number>} The number of rows affected
787
   * @throws {Error} If the delete operation fails
788
   */
789
  deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number>;
790

791
  /**
792
   * Updates a record by its ID with optimistic locking support.
793
   * If a version field is defined in the schema, versioning is applied:
794
   * - the current record version is retrieved
795
   * - checked for concurrent modifications
796
   * - and then incremented
797
   *
798
   * @template T - The type of the table schema
799
   * @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
800
   * @param {T} schema - The entity schema
801
   * @returns {Promise<number>} The number of rows affected
802
   * @throws {Error} If the primary key is not included in the update fields
803
   * @throws {Error} If optimistic locking check fails
804
   */
805
  updateById<T extends AnyMySqlTable>(
806
    entity: Partial<InferInsertModel<T>>,
807
    schema: T,
808
  ): Promise<number>;
809

810
  /**
811
   * Updates specified fields of records based on provided conditions.
812
   * If the "where" parameter is not provided, the WHERE clause is built from the entity fields
813
   * that are not included in the list of fields to update.
814
   *
815
   * @template T - The type of the table schema
816
   * @param {Partial<InferInsertModel<T>>} updateData - The object containing values to update
817
   * @param {T} schema - The entity schema
818
   * @param {SQL<unknown>} [where] - Optional filtering conditions for the WHERE clause
819
   * @returns {Promise<number>} The number of affected rows
820
   * @throws {Error} If no filtering criteria are provided
821
   * @throws {Error} If the update operation fails
822
   */
823
  updateFields<T extends AnyMySqlTable>(
824
    updateData: Partial<InferInsertModel<T>>,
825
    schema: T,
826
    where?: SQL<unknown>,
827
  ): Promise<number>;
828
}
829

830
export interface CacheForgeSQL extends VerioningModificationForgeSQL {
831
  evictCache(tables: string[]): Promise<void>;
832
  evictCacheEntities(tables: AnyMySqlTable[]): Promise<void>;
833
}
834

835
/**
836
 * Interface for schema analysis operations.
837
 * Provides methods for analyzing query performance and execution plans.
838
 *
839
 * @interface SchemaAnalyzeForgeSql
840
 */
841
export interface SchemaAnalyzeForgeSql {
842
  /**
843
   * Executes EXPLAIN on a Drizzle query.
844
   * @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
845
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
846
   */
847
  explain(query: { toSQL: () => Query }): Promise<ExplainAnalyzeRow[]>;
848

849
  /**
850
   * Executes EXPLAIN on a raw SQL query.
851
   * @param {string} query - The SQL query to analyze
852
   * @param {unknown[]} bindParams - The query parameters
853
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
854
   */
855
  explainRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]>;
856

857
  /**
858
   * Executes EXPLAIN ANALYZE on a Drizzle query.
859
   * @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
860
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
861
   */
862
  explainAnalyze(query: { toSQL: () => Query }): Promise<ExplainAnalyzeRow[]>;
863

864
  /**
865
   * Executes EXPLAIN ANALYZE on a raw SQL query.
866
   * @param {string} query - The SQL query to analyze
867
   * @param {unknown[]} bindParams - The query parameters
868
   * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
869
   */
870
  explainAnalyzeRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]>;
871

872
  /**
873
   * Analyzes slow queries from the database.
874
   * @returns {Promise<SlowQueryNormalized[]>} The normalized slow query data
875
   */
876
  analyzeSlowQueries(): Promise<SlowQueryNormalized[]>;
877

878
  /**
879
   * Analyzes query history for specific tables using Drizzle table objects.
880
   * @param {AnyMySqlTable[]} tables - The Drizzle table objects to analyze
881
   * @param {Date} [fromDate] - The start date for the analysis
882
   * @param {Date} [toDate] - The end date for the analysis
883
   * @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
884
   */
885
  analyzeQueriesHistory(
886
    tables: AnyMySqlTable[],
887
    fromDate?: Date,
888
    toDate?: Date,
889
  ): Promise<ClusterStatementRowCamelCase[]>;
890

891
  /**
892
   * Analyzes query history for specific tables using raw table names.
893
   * @param {string[]} tables - The table names to analyze
894
   * @param {Date} [fromDate] - The start date for the analysis
895
   * @param {Date} [toDate] - The end date for the analysis
896
   * @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
897
   */
898
  analyzeQueriesHistoryRaw(
899
    tables: string[],
900
    fromDate?: Date,
901
    toDate?: Date,
902
  ): Promise<ClusterStatementRowCamelCase[]>;
903
}
904

905
/**
906
 * Interface for schema-level SQL operations.
907
 * Provides methods for executing SQL queries with schema binding and type safety.
908
 *
909
 * @interface SchemaSqlForgeSql
910
 */
911
export interface SchemaSqlForgeSql {
912
  /**
913
   * Executes a Drizzle query and returns a single result.
914
   * @template T - The type of the query builder
915
   * @param {T} query - The Drizzle query to execute
916
   * @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined
917
   * @throws {Error} If more than one record is returned
918
   * @throws {Error} If the query execution fails
919
   */
920
  executeQueryOnlyOne<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(
921
    query: T,
922
  ): Promise<
923
    Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined
924
  >;
925

926
  /**
927
   * Executes a raw SQL query and returns the results.
928
   * @template T - The type of the result objects
929
   * @param {string} query - The raw SQL query
930
   * @param {SqlParameters[]} [params] - Optional SQL parameters
931
   * @returns {Promise<T[]>} A list of results as objects
932
   * @throws {Error} If the query execution fails
933
   */
934
  executeRawSQL<T extends object | unknown>(query: string, params?: SqlParameters[]): Promise<T[]>;
935

936
  /**
937
   * Executes a raw SQL update query.
938
   *
939
   * @param query - The raw SQL update query
940
   * @param params - Optional SQL parameters
941
   * @returns Promise that resolves to the update response containing affected rows
942
   * @throws Error if the update operation fails
943
   */
944
  executeRawUpdateSQL(query: string, params?: unknown[]): Promise<UpdateQueryResponse>;
945
}
946

947
/**
948
 * Interface for version field metadata.
949
 * Defines the configuration for optimistic locking version fields.
950
 *
951
 * @interface VersionFieldMetadata
952
 */
953
export interface VersionFieldMetadata {
954
  /** Name of the version field */
955
  fieldName: string;
956
}
957

958
/**
959
 * Interface for table metadata.
960
 * Defines the configuration for a specific table.
961
 *
962
 * @interface TableMetadata
963
 */
964
export interface TableMetadata {
965
  /** Name of the table */
966
  tableName: string;
967
  /** Version field configuration for optimistic locking */
968
  versionField: VersionFieldMetadata;
969
}
970

971
/**
972
 * Type for additional metadata configuration.
973
 * Maps table names to their metadata configuration.
974
 *
975
 * @type {AdditionalMetadata}
976
 */
977
export type AdditionalMetadata = Record<string, TableMetadata>;
978

979
/**
980
 * Interface for ForgeSQL ORM options
981
 *
982
 * @interface ForgeSqlOrmOptions
983
 */
984
export interface ForgeSqlOrmOptions {
985
  /** Whether to log raw SQL queries to the console */
986
  logRawSqlQuery?: boolean;
987
  /** Whether to log cache operations (hits, misses, evictions) */
988
  logCache?: boolean;
989
  /** Whether to disable optimistic locking for update operations */
990
  disableOptimisticLocking?: boolean;
991
  /** SQL hints to be applied to queries for optimization */
992
  hints?: SqlHints;
993
  /** Default Cache TTL (Time To Live) in seconds */
994
  cacheTTL?: number;
995
  /** Name of the KVS entity used for cache storage */
996
  cacheEntityName?: string;
997
  /** Name of the field in cache entity that stores SQL query */
998
  cacheEntityQueryName?: string;
999
  /** Whether to wrap table names with backticks in cache keys */
1000
  cacheWrapTable?: boolean;
1001
  /** Name of the field in cache entity that stores expiration timestamp */
1002
  cacheEntityExpirationName?: string;
1003
  /** Name of the field in cache entity that stores cached data */
1004
  cacheEntityDataName?: string;
1005

1006
  /**
1007
   * Additional metadata for table configuration.
1008
   * Allows specifying table-specific settings and behaviors such as version fields for optimistic locking.
1009
   *
1010
   * @example
1011
   * ```typescript
1012
   * {
1013
   *   users: {
1014
   *     tableName: "users",
1015
   *     versionField: {
1016
   *       fieldName: "updatedAt",
1017
   *       type: "datetime",
1018
   *       nullable: false
1019
   *     }
1020
   *   }
1021
   * }
1022
   * ```
1023
   */
1024
  additionalMetadata?: AdditionalMetadata;
1025
}
1026

1027
/**
1028
 * Custom type for MySQL datetime fields.
1029
 * Handles conversion between JavaScript Date objects and MySQL datetime strings.
1030
 *
1031
 * @type {CustomType}
1032
 */
1033
export const forgeDateTimeString = customType<{
1✔
1034
  data: Date;
1035
  driver: string;
1036
  config: { format?: string };
1037
}>({
1✔
1038
  dataType() {
1✔
1039
    return "datetime";
4✔
1040
  },
4✔
1041
  toDriver(value: Date) {
1✔
1042
    return formatDateTime(value, "yyyy-MM-dd' 'HH:mm:ss.SSS", false);
11✔
1043
  },
11✔
1044
  fromDriver(value: unknown) {
1✔
1045
    const format = "yyyy-MM-dd' 'HH:mm:ss.SSS";
11✔
1046
    return parseDateTime(value as string, format);
11✔
1047
  },
11✔
1048
});
1✔
1049

1050
/**
1051
 * Custom type for MySQL timestamp fields.
1052
 * Handles conversion between JavaScript Date objects and MySQL timestamp strings.
1053
 *
1054
 * @type {CustomType}
1055
 */
1056
export const forgeTimestampString = customType<{
1✔
1057
  data: Date;
1058
  driver: string;
1059
  config: { format?: string };
1060
}>({
1✔
1061
  dataType() {
1✔
1062
    return "timestamp";
1✔
1063
  },
1✔
1064
  toDriver(value: Date) {
1✔
1065
    return formatDateTime(value, "yyyy-MM-dd' 'HH:mm:ss.SSS", true);
2✔
1066
  },
2✔
1067
  fromDriver(value: unknown) {
1✔
1068
    const format = "yyyy-MM-dd' 'HH:mm:ss.SSS";
×
1069
    return parseDateTime(value as string, format);
×
1070
  },
×
1071
});
1✔
1072

1073
/**
1074
 * Custom type for MySQL date fields.
1075
 * Handles conversion between JavaScript Date objects and MySQL date strings.
1076
 *
1077
 * @type {CustomType}
1078
 */
1079
export const forgeDateString = customType<{
1✔
1080
  data: Date;
1081
  driver: string;
1082
  config: { format?: string };
1083
}>({
1✔
1084
  dataType() {
1✔
1085
    return "date";
×
1086
  },
×
1087
  toDriver(value: Date) {
1✔
1088
    return formatDateTime(value, "yyyy-MM-dd", false);
×
1089
  },
×
1090
  fromDriver(value: unknown) {
1✔
1091
    const format = "yyyy-MM-dd";
×
1092
    return parseDateTime(value as string, format);
×
1093
  },
×
1094
});
1✔
1095

1096
/**
1097
 * Custom type for MySQL time fields.
1098
 * Handles conversion between JavaScript Date objects and MySQL time strings.
1099
 *
1100
 * @type {CustomType}
1101
 */
1102
export const forgeTimeString = customType<{
1✔
1103
  data: Date;
1104
  driver: string;
1105
  config: { format?: string };
1106
}>({
1✔
1107
  dataType() {
1✔
1108
    return "time";
×
1109
  },
×
1110
  toDriver(value: Date) {
1✔
1111
    return formatDateTime(value, "HH:mm:ss.SSS", false);
×
1112
  },
×
1113
  fromDriver(value: unknown) {
1✔
1114
    return parseDateTime(value as string, "HH:mm:ss.SSS");
×
1115
  },
×
1116
});
1✔
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