forge-sql-orm
Version:
Drizzle ORM integration for Atlassian @forge/sql. Provides a custom driver, schema migration, two levels of caching (local and global via @forge/kvs), optimistic locking, and query analysis.
1,196 lines (1,150 loc) • 62.1 kB
text/typescript
import { ForgeSQLCrudOperations } from "./ForgeSQLCrudOperations";
import {
VerioningModificationForgeSQL,
ForgeSqlOperation,
ForgeSqlOrmOptions,
SchemaAnalyzeForgeSql,
SchemaSqlForgeSql,
RovoIntegration,
} from "./ForgeSQLQueryBuilder";
import { ForgeSQLSelectOperations } from "./ForgeSQLSelectOperations";
import {
drizzle,
MySqlRemoteDatabase,
MySqlRemotePreparedQueryHKT,
MySqlRemoteQueryResultHKT,
} from "drizzle-orm/mysql-proxy";
import { createForgeDriverProxy } from "../utils/forgeDriverProxy";
import type { SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
import { MySqlSelectBuilder } from "drizzle-orm/mysql-core";
import {
DeleteAndEvictCacheType,
InsertAndEvictCacheType,
patchDbWithSelectAliased,
SelectAliasedCacheableType,
SelectAliasedDistinctCacheableType,
SelectAliasedDistinctType,
SelectAliasedType,
UpdateAndEvictCacheType,
} from "../lib";
import { ForgeSQLAnalyseOperation } from "./ForgeSQLAnalyseOperations";
import { ForgeSQLCacheOperations } from "./ForgeSQLCacheOperations";
import { MySqlTable } from "drizzle-orm/mysql-core/table";
import {
MySqlDeleteBase,
MySqlInsertBuilder,
MySqlUpdateBuilder,
} from "drizzle-orm/mysql-core/query-builders";
import { cacheApplicationContext, localCacheApplicationContext } from "../utils/cacheContextUtils";
import { clearTablesCache } from "../utils/cacheUtils";
import { SQLWrapper } from "drizzle-orm/sql/sql";
import { WithSubquery } from "drizzle-orm/subquery";
import {
getLastestMetadata,
metadataQueryContext,
MetadataQueryOptions,
} from "../utils/metadataContextUtils";
import { operationTypeQueryContext } from "../utils/requestTypeContextUtils";
import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
import { Rovo } from "./Rovo";
/**
* Implementation of ForgeSQLORM that uses Drizzle ORM for query building.
* This class provides a bridge between Forge SQL and Drizzle ORM, allowing
* to use Drizzle's query builder while executing queries through Forge SQL.
*/
class ForgeSQLORMImpl implements ForgeSqlOperation {
private static instance: ForgeSQLORMImpl | null = null;
private readonly drizzle: MySqlRemoteDatabase<any> & {
selectAliased: SelectAliasedType;
selectAliasedDistinct: SelectAliasedDistinctType;
selectAliasedCacheable: SelectAliasedCacheableType;
selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
insertWithCacheContext: InsertAndEvictCacheType;
insertAndEvictCache: InsertAndEvictCacheType;
updateAndEvictCache: UpdateAndEvictCacheType;
updateWithCacheContext: UpdateAndEvictCacheType;
deleteAndEvictCache: DeleteAndEvictCacheType;
deleteWithCacheContext: DeleteAndEvictCacheType;
};
private readonly crudOperations: VerioningModificationForgeSQL;
private readonly fetchOperations: SchemaSqlForgeSql;
private readonly analyzeOperations: SchemaAnalyzeForgeSql;
private readonly cacheOperations: ForgeSQLCacheOperations;
private readonly options: ForgeSqlOrmOptions;
/**
* Private constructor to enforce singleton behavior.
* @param options - Options for configuring ForgeSQL ORM behavior.
*/
private constructor(options?: ForgeSqlOrmOptions) {
try {
const newOptions: ForgeSqlOrmOptions = options ?? {
logRawSqlQuery: false,
logCache: false,
disableOptimisticLocking: false,
cacheWrapTable: true,
cacheTTL: 120,
cacheEntityQueryName: "sql",
cacheEntityExpirationName: "expiration",
cacheEntityDataName: "data",
};
this.options = newOptions;
if (newOptions.logRawSqlQuery) {
// eslint-disable-next-line no-console
console.debug("Initializing ForgeSQLORM...");
}
// Initialize Drizzle instance with our custom driver
const proxiedDriver = createForgeDriverProxy(
this,
newOptions.hints,
newOptions.logRawSqlQuery,
);
this.drizzle = patchDbWithSelectAliased(
drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
newOptions,
);
this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
} catch (error) {
// eslint-disable-next-line no-console
console.error("ForgeSQLORM initialization failed:", error);
throw error;
}
}
/**
* Executes a query and provides access to execution metadata with performance monitoring.
* This method allows you to capture detailed information about query execution
* including database execution time, response size, and query analysis capabilities.
*
* The method aggregates metrics across all database operations within the query function,
* making it ideal for monitoring resolver performance and detecting performance issues.
*
* @template T - The return type of the query
* @param query - A function that returns a Promise with the query result. Can contain multiple database operations.
* @param onMetadata - Callback function that receives aggregated execution metadata
* @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
* @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
* @param onMetadata.printQueriesWithPlan - Function to analyze and print query execution plans. Supports two modes:
* - TopSlowest: Prints execution plans for the slowest queries from the current resolver (default)
* - SummaryTable: Uses CLUSTER_STATEMENTS_SUMMARY if within time window
* @param options - Optional configuration for query plan printing behavior
* @param options.mode - Query plan printing mode: 'TopSlowest' (default) or 'SummaryTable'
* @param options.summaryTableWindowTime - Time window in milliseconds for summary table queries (default: 15000ms). Only used when mode is 'SummaryTable'
* @param options.topQueries - Number of top slowest queries to analyze when mode is 'TopSlowest' (default: 1)
* @param options.showSlowestPlans - Whether to show execution plans for slowest queries in TopSlowest mode (default: true)
* @param options.normalizeQuery - Whether to normalize SQL queries by replacing parameter values with '?' placeholders (default: true). Set to false to disable normalization if it causes issues
* @returns Promise with the query result
*
* @example
* ```typescript
* // Basic usage with performance monitoring
* const result = await forgeSQL.executeWithMetadata(
* async () => {
* const users = await forgeSQL.selectFrom(usersTable);
* const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
* return { users, orders };
* },
* (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
* const threshold = 500; // ms baseline for this resolver
*
* if (totalDbExecutionTime > threshold * 1.5) {
* console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
* await printQueriesWithPlan(); // Analyze and print query execution plans
* } else if (totalDbExecutionTime > threshold) {
* console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
* }
*
* console.log(`DB response size: ${totalResponseSize} bytes`);
* }
* );
* ```
*
* @example
* ```typescript
* // Resolver with performance monitoring
* resolver.define("fetch", async (req: Request) => {
* try {
* return await forgeSQL.executeWithMetadata(
* async () => {
* // Resolver logic with multiple queries
* const users = await forgeSQL.selectFrom(demoUsers);
* const orders = await forgeSQL.selectFrom(demoOrders)
* .where(eq(demoOrders.userId, demoUsers.id));
* return { users, orders };
* },
* async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
* const threshold = 500; // ms baseline for this resolver
*
* if (totalDbExecutionTime > threshold * 1.5) {
* console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
* await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
* } else if (totalDbExecutionTime > threshold) {
* console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
* }
*
* console.log(`DB response size: ${totalResponseSize} bytes`);
* }
* );
* } catch (e) {
* const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
* console.error(error, e);
* throw error;
* }
* });
* ```
*
* @example
* ```typescript
* // Using TopSlowest mode with custom topQueries
* const result = await forgeSQL.executeWithMetadata(
* async () => {
* const users = await forgeSQL.selectFrom(usersTable);
* return users;
* },
* async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
* if (totalDbExecutionTime > 1000) {
* await printQueriesWithPlan(); // Will print top 3 slowest queries
* }
* },
* {
* mode: 'TopSlowest', // Print top slowest queries (default)
* topQueries: 3, // Print top 3 slowest queries
* }
* );
* ```
*
* @example
* ```typescript
* // Using SummaryTable mode for query analysis
* const result = await forgeSQL.executeWithMetadata(
* async () => {
* const users = await forgeSQL.selectFrom(usersTable);
* return users;
* },
* async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
* if (totalDbExecutionTime > 1000) {
* await printQueriesWithPlan(); // Will use CLUSTER_STATEMENTS_SUMMARY if within time window
* }
* },
* {
* mode: 'SummaryTable', // Use summary tables mode
* summaryTableWindowTime: 10000, // 10 second window
* }
* );
* ```
*
* @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueriesWithPlan()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
*/
async executeWithMetadata<T>(
query: () => Promise<T>,
onMetadata: (
totalDbExecutionTime: number,
totalResponseSize: number,
printQueriesWithPlan: () => Promise<void>,
) => Promise<void> | void,
options?: MetadataQueryOptions,
): Promise<T> {
return metadataQueryContext.run(
{
totalDbExecutionTime: 0,
totalResponseSize: 0,
beginTime: new Date(),
forgeSQLORM: this,
printQueriesWithPlan: async () => {
return;
},
options: options,
statistics: [],
},
async () => {
const result = await query();
const metadata = await getLastestMetadata();
try {
if (metadata) {
await onMetadata(
metadata.totalDbExecutionTime,
metadata.totalResponseSize,
metadata.printQueriesWithPlan,
);
}
} catch (e: any) {
// eslint-disable-next-line no-console
console.error(
"[ForgeSQLORM][executeWithMetadata] Failed to run onMetadata callback",
{
errorMessage: e?.message,
errorStack: e?.stack,
totalDbExecutionTime: metadata?.totalDbExecutionTime,
totalResponseSize: metadata?.totalResponseSize,
beginTime: metadata?.beginTime,
},
e,
);
}
return result;
},
);
}
/**
* Executes operations within a cache context that collects cache eviction events.
* All clearCache calls within the context are collected and executed in batch at the end.
* Queries executed within this context will bypass cache for tables that were marked for clearing.
*
* This is useful for:
* - Batch operations that affect multiple tables
* - Transaction-like operations where you want to clear cache only at the end
* - Performance optimization by reducing cache clear operations
*
* @param cacheContext - Function containing operations that may trigger cache evictions
* @returns Promise that resolves when all operations and cache clearing are complete
*
* @example
* ```typescript
* await forgeSQL.executeWithCacheContext(async () => {
* await forgeSQL.modifyWithVersioning().insert(users, userData);
* await forgeSQL.modifyWithVersioning().insert(orders, orderData);
* // Cache for both users and orders tables will be cleared at the end
* });
* ```
*/
executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void> {
return this.executeWithCacheContextAndReturnValue<void>(cacheContext);
}
/**
* Executes operations within a cache context and returns a value.
* All clearCache calls within the context are collected and executed in batch at the end.
* Queries executed within this context will bypass cache for tables that were marked for clearing.
*
* @param cacheContext - Function containing operations that may trigger cache evictions
* @returns Promise that resolves to the return value of the cacheContext function
*
* @example
* ```typescript
* const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
* await forgeSQL.modifyWithVersioning().insert(users, userData);
* return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
* });
* ```
*/
async executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
return await this.executeWithLocalCacheContextAndReturnValue(
async () =>
await cacheApplicationContext.run(
cacheApplicationContext.getStore() ?? { tables: new Set<string>() },
async () => {
try {
return await cacheContext();
} finally {
await clearTablesCache(
Array.from(cacheApplicationContext.getStore()?.tables ?? []),
this.options,
);
}
},
),
);
}
/**
* Executes operations within a local cache context and returns a value.
* This provides in-memory caching for select queries within a single request scope.
*
* @param cacheContext - Function containing operations that will benefit from local caching
* @returns Promise that resolves to the return value of the cacheContext function
*/
async executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
return await localCacheApplicationContext.run(
localCacheApplicationContext.getStore() ?? { cache: {} },
async () => {
return await cacheContext();
},
);
}
/**
* Executes operations within a local cache context.
* This provides in-memory caching for select queries within a single request scope.
*
* @param cacheContext - Function containing operations that will benefit from local caching
* @returns Promise that resolves when all operations are complete
*/
executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void> {
return this.executeWithLocalCacheContextAndReturnValue<void>(cacheContext);
}
/**
* Creates an insert query builder.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
*
* @param table - The table to insert into
* @returns Insert query builder (no versioning, no cache management)
*/
insert<TTable extends MySqlTable>(
table: TTable,
): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.drizzle.insertWithCacheContext(table);
}
/**
* Creates an insert query builder that automatically evicts cache after execution.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
*
* @param table - The table to insert into
* @returns Insert query builder with automatic cache eviction (no versioning)
*/
insertAndEvictCache<TTable extends MySqlTable>(
table: TTable,
): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.drizzle.insertAndEvictCache(table);
}
/**
* Creates an update query builder that automatically evicts cache after execution.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
*
* @param table - The table to update
* @returns Update query builder with automatic cache eviction (no versioning)
*/
updateAndEvictCache<TTable extends MySqlTable>(
table: TTable,
): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.drizzle.updateAndEvictCache(table);
}
/**
* Creates an update query builder.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
*
* @param table - The table to update
* @returns Update query builder (no versioning, no cache management)
*/
update<TTable extends MySqlTable>(
table: TTable,
): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.drizzle.updateWithCacheContext(table);
}
/**
* Creates a delete query builder.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
*
* @param table - The table to delete from
* @returns Delete query builder (no versioning, no cache management)
*/
delete<TTable extends MySqlTable>(
table: TTable,
): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.drizzle.deleteWithCacheContext(table);
}
/**
* Creates a delete query builder that automatically evicts cache after execution.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
*
* @param table - The table to delete from
* @returns Delete query builder with automatic cache eviction (no versioning)
*/
deleteAndEvictCache<TTable extends MySqlTable>(
table: TTable,
): MySqlDeleteBase<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.drizzle.deleteAndEvictCache(table);
}
/**
* Create the modify operations instance.
* @returns modify operations.
*/
modifyWithVersioning(): VerioningModificationForgeSQL {
return this.crudOperations;
}
/**
* Returns the singleton instance of ForgeSQLORMImpl.
* @param options - Options for configuring ForgeSQL ORM behavior.
* @returns The singleton instance of ForgeSQLORMImpl.
*/
static getInstance(options?: ForgeSqlOrmOptions): ForgeSqlOperation {
ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
return ForgeSQLORMImpl.instance;
}
/**
* Retrieves the fetch operations instance.
* @returns Fetch operations.
*/
fetch(): SchemaSqlForgeSql {
return this.fetchOperations;
}
/**
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
*/
analyze(): SchemaAnalyzeForgeSql {
return this.analyzeOperations;
}
/**
* Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
*
* This method returns operations that use `modifyWithVersioning()` internally, providing:
* - Optimistic locking support
* - Automatic version field management
* - Cache eviction after successful operations
*
* @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
*/
modifyWithVersioningAndEvictCache(): ForgeSQLCacheOperations {
return this.cacheOperations;
}
/**
* Returns a Drizzle query builder instance.
*
* ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
* The returned instance should NOT be used for direct database connections or query execution.
* All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
*
* @returns A Drizzle query builder instance for query construction only.
*/
getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
selectAliased: SelectAliasedType;
selectAliasedDistinct: SelectAliasedDistinctType;
selectAliasedCacheable: SelectAliasedCacheableType;
selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
insertWithCacheContext: InsertAndEvictCacheType;
insertAndEvictCache: InsertAndEvictCacheType;
updateAndEvictCache: UpdateAndEvictCacheType;
updateWithCacheContext: UpdateAndEvictCacheType;
deleteAndEvictCache: DeleteAndEvictCacheType;
deleteWithCacheContext: DeleteAndEvictCacheType;
} {
return this.drizzle;
}
/**
* Creates a select query with unique field aliases to prevent field name collisions in joins.
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
*
* @template TSelection - The type of the selected fields
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
* @throws {Error} If fields parameter is empty
* @example
* ```typescript
* await forgeSQL
* .select({user: users, order: orders})
* .from(orders)
* .innerJoin(users, eq(orders.userId, users.id));
* ```
*/
select<TSelection extends SelectedFields>(
fields: TSelection,
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
if (!fields) {
throw new Error("fields is empty");
}
return this.drizzle.selectAliased(fields);
}
/**
* Creates a distinct select query with unique field aliases to prevent field name collisions in joins.
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
*
* @template TSelection - The type of the selected fields
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
* @throws {Error} If fields parameter is empty
* @example
* ```typescript
* await forgeSQL
* .selectDistinct({user: users, order: orders})
* .from(orders)
* .innerJoin(users, eq(orders.userId, users.id));
* ```
*/
selectDistinct<TSelection extends SelectedFields>(
fields: TSelection,
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
if (!fields) {
throw new Error("fields is empty");
}
return this.drizzle.selectAliasedDistinct(fields);
}
/**
* Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
*
* @template TSelection - The type of the selected fields
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
* @throws {Error} If fields parameter is empty
* @example
* ```typescript
* await forgeSQL
* .selectCacheable({user: users, order: orders},60)
* .from(orders)
* .innerJoin(users, eq(orders.userId, users.id));
* ```
*/
selectCacheable<TSelection extends SelectedFields>(
fields: TSelection,
cacheTTL?: number,
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
if (!fields) {
throw new Error("fields is empty");
}
return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
}
/**
* Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
* This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
*
* @template TSelection - The type of the selected fields
* @param {TSelection} fields - Object containing the fields to select, with table schemas as values
* @param {number} cacheTTL - cache ttl optional default is 60 sec.
* @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
* @throws {Error} If fields parameter is empty
* @example
* ```typescript
* await forgeSQL
* .selectDistinctCacheable({user: users, order: orders}, 60)
* .from(orders)
* .innerJoin(users, eq(orders.userId, users.id));
* ```
*/
selectDistinctCacheable<TSelection extends SelectedFields>(
fields: TSelection,
cacheTTL?: number,
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
if (!fields) {
throw new Error("fields is empty");
}
return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
}
/**
* Creates a select query builder for all columns from a table with field aliasing support.
* This is a convenience method that automatically selects all columns from the specified table.
*
* @template T - The type of the table
* @param table - The table to select from
* @returns Select query builder with all table columns and field aliasing support
* @example
* ```typescript
* const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
* ```
*/
selectFrom<T extends MySqlTable>(table: T) {
return this.drizzle.selectFrom(table);
}
/**
* Creates a select distinct query builder for all columns from a table with field aliasing support.
* This is a convenience method that automatically selects all distinct columns from the specified table.
*
* @template T - The type of the table
* @param table - The table to select from
* @returns Select distinct query builder with all table columns and field aliasing support
* @example
* ```typescript
* const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
* ```
*/
selectDistinctFrom<T extends MySqlTable>(table: T) {
return this.drizzle.selectDistinctFrom(table);
}
/**
* Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
* This is a convenience method that automatically selects all columns from the specified table with caching enabled.
*
* @template T - The type of the table
* @param table - The table to select from
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
* @returns Select query builder with all table columns, field aliasing, and caching support
* @example
* ```typescript
* const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
* ```
*/
selectCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
return this.drizzle.selectFromCacheable(table, cacheTTL);
}
/**
* Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
* This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
*
* @template T - The type of the table
* @param table - The table to select from
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
* @returns Select distinct query builder with all table columns, field aliasing, and caching support
* @example
* ```typescript
* const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
* ```
*/
selectDistinctCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
return this.drizzle.selectDistinctFromCacheable(table, cacheTTL);
}
/**
* Executes a raw SQL query with local cache support.
* This method provides local caching for raw SQL queries within the current invocation context.
* Results are cached locally and will be returned from cache on subsequent identical queries.
*
* @param query - The SQL query to execute (SQLWrapper or string)
* @returns Promise with query results
* @example
* ```typescript
* // Using SQLWrapper
* const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
*
* // Using string
* const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
* ```
*/
execute<T>(query: SQLWrapper | string) {
return this.drizzle.executeQuery<T>(query);
}
/**
* Executes a Data Definition Language (DDL) SQL query.
* DDL operations include CREATE, ALTER, DROP, TRUNCATE, and other schema modification statements.
*
* This method is specifically designed for DDL operations and provides:
* - Proper operation type context for DDL queries
* - No caching (DDL operations should not be cached)
* - Direct execution without query optimization
*
* @template T - The expected return type of the query result
* @param query - The DDL SQL query to execute (SQLWrapper or string)
* @returns Promise with query results
* @throws {Error} If the DDL operation fails
*
* @example
* ```typescript
* // Create a new table
* await forgeSQL.executeDDL(`
* CREATE TABLE users (
* id INT PRIMARY KEY AUTO_INCREMENT,
* name VARCHAR(255) NOT NULL,
* email VARCHAR(255) UNIQUE
* )
* `);
*
* // Alter table structure
* await forgeSQL.executeDDL(sql`
* ALTER TABLE users
* ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
* `);
*
* // Drop a table
* await forgeSQL.executeDDL("DROP TABLE IF EXISTS old_users");
* ```
*/
async executeDDL<T>(query: SQLWrapper | string) {
return this.executeDDLActions(async () => this.drizzle.executeQuery<T>(query));
}
/**
* Executes a series of actions within a DDL operation context.
* This method provides a way to execute regular SQL queries that should be treated
* as DDL operations, ensuring proper operation type context for performance monitoring.
*
* This method is useful for:
* - Executing regular SQL queries in DDL context for monitoring purposes
* - Wrapping non-DDL operations that should be treated as DDL for analysis
* - Ensuring proper operation type context for complex workflows
* - Maintaining DDL operation context across multiple function calls
*
* @template T - The return type of the actions function
* @param actions - Function containing SQL operations to execute in DDL context
* @returns Promise that resolves to the return value of the actions function
*
* @example
* ```typescript
* // Execute regular SQL queries in DDL context for monitoring
* await forgeSQL.executeDDLActions(async () => {
* const slowQueries = await forgeSQL.execute(`
* SELECT * FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
* WHERE AVG_LATENCY > 1000000
* `);
* return slowQueries;
* });
*
* // Execute complex analysis queries in DDL context
* const result = await forgeSQL.executeDDLActions(async () => {
* const tableInfo = await forgeSQL.execute("SHOW TABLES");
* const performanceData = await forgeSQL.execute(`
* SELECT * FROM INFORMATION_SCHEMA.CLUSTER_STATEMENTS_SUMMARY_HISTORY
* WHERE SUMMARY_END_TIME > DATE_SUB(NOW(), INTERVAL 1 HOUR)
* `);
* return { tableInfo, performanceData };
* });
*
* // Execute monitoring queries with error handling
* try {
* await forgeSQL.executeDDLActions(async () => {
* const metrics = await forgeSQL.execute(`
* SELECT COUNT(*) as query_count
* FROM INFORMATION_SCHEMA.STATEMENTS_SUMMARY
* `);
* console.log(`Total queries: ${metrics[0].query_count}`);
* });
* } catch (error) {
* console.error("Monitoring query failed:", error);
* }
* ```
*/
async executeDDLActions<T>(actions: () => Promise<T>): Promise<T> {
return operationTypeQueryContext.run({ operationType: "DDL" }, async () => actions());
}
/**
* Executes a raw SQL query with both local and global cache support.
* This method provides comprehensive caching for raw SQL queries:
* - Local cache: Within the current invocation context
* - Global cache: Cross-invocation caching using @forge/kvs
*
* @param query - The SQL query to execute (SQLWrapper or string)
* @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
* @returns Promise with query results
* @example
* ```typescript
* // Using SQLWrapper with custom TTL
* const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
*
* // Using string with default TTL
* const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
* ```
*/
executeCacheable<T>(
query: SQLWrapper | string,
cacheTtl?: number,
): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>> {
return this.drizzle.executeQueryCacheable<T>(query, cacheTtl);
}
/**
* Creates a Common Table Expression (CTE) builder for complex queries.
* CTEs allow you to define temporary named result sets that exist within the scope of a single query.
*
* @returns WithBuilder for creating CTEs
* @example
* ```typescript
* const withQuery = forgeSQL.$with('userStats').as(
* forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
* .from(users)
* .groupBy(users.id)
* );
* ```
*/
get $with() {
return this.drizzle.$with;
}
/**
* Creates a query builder that uses Common Table Expressions (CTEs).
* CTEs allow you to define temporary named result sets that exist within the scope of a single query.
*
* @param queries - Array of CTE queries created with $with()
* @returns Query builder with CTE support
* @example
* ```typescript
* const withQuery = forgeSQL.$with('userStats').as(
* forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
* .from(users)
* .groupBy(users.id)
* );
*
* const result = await forgeSQL.with(withQuery)
* .select({ userId: withQuery.userId, count: withQuery.count })
* .from(withQuery);
* ```
*/
with(...queries: WithSubquery[]) {
return this.drizzle.with(...queries);
}
/**
* Provides access to Rovo integration - a secure pattern for natural-language analytics.
*
* Rovo enables secure execution of dynamic SQL queries with comprehensive security validations:
* - Only SELECT queries are allowed
* - Queries are restricted to a single table
* - JOINs, subqueries, and window functions are blocked
* - Row-Level Security (RLS) support for data isolation
*
* @returns {RovoIntegration} Rovo integration instance for secure dynamic queries
*
* @example
* ```typescript
* const rovo = forgeSQL.rovo();
* const settings = await rovo.rovoSettingBuilder(usersTable, accountId)
* .useRLS()
* .addRlsColumn(usersTable.id)
* .addRlsWherePart((alias) => `${alias}.id = '${accountId}'`)
* .finish()
* .build();
*
* const result = await rovo.dynamicIsolatedQuery(
* "SELECT id, name FROM users WHERE status = 'active'",
* settings
* );
* ```
*/
rovo(): RovoIntegration {
return new Rovo(this, this.options);
}
}
/**
* Public class that acts as a wrapper around the private ForgeSQLORMImpl.
* Provides a clean interface for working with Forge SQL and Drizzle ORM.
*/
class ForgeSQLORM implements ForgeSqlOperation {
private readonly ormInstance: ForgeSqlOperation;
constructor(options?: ForgeSqlOrmOptions) {
this.ormInstance = ForgeSQLORMImpl.getInstance(options);
}
/**
* Executes a query and provides access to execution metadata with performance monitoring.
* This method allows you to capture detailed information about query execution
* including database execution time, response size, and query analysis capabilities.
*
* The method aggregates metrics across all database operations within the query function,
* making it ideal for monitoring resolver performance and detecting performance issues.
*
* @template T - The return type of the query
* @param query - A function that returns a Promise with the query result. Can contain multiple database operations.
* @param onMetadata - Callback function that receives aggregated execution metadata
* @param onMetadata.totalDbExecutionTime - Total database execution time across all operations in the query function (in milliseconds)
* @param onMetadata.totalResponseSize - Total response size across all operations (in bytes)
* @param onMetadata.printQueriesWithPlan - Function to analyze and print query execution plans. Supports two modes:
* - TopSlowest: Prints execution plans for the slowest queries from the current resolver (default)
* - SummaryTable: Uses CLUSTER_STATEMENTS_SUMMARY if within time window
* @param options - Optional configuration for query plan printing behavior
* @param options.mode - Query plan printing mode: 'TopSlowest' (default) or 'SummaryTable'
* @param options.summaryTableWindowTime - Time window in milliseconds for summary table queries (default: 15000ms). Only used when mode is 'SummaryTable'
* @param options.topQueries - Number of top slowest queries to analyze when mode is 'TopSlowest' (default: 1)
* @param options.showSlowestPlans - Whether to show execution plans for slowest queries in TopSlowest mode (default: true)
* @param options.normalizeQuery - Whether to normalize SQL queries by replacing parameter values with '?' placeholders (default: true). Set to false to disable normalization if it causes issues
* @returns Promise with the query result
*
* @example
* ```typescript
* // Basic usage with performance monitoring
* const result = await forgeSQL.executeWithMetadata(
* async () => {
* const users = await forgeSQL.selectFrom(usersTable);
* const orders = await forgeSQL.selectFrom(ordersTable).where(eq(ordersTable.userId, usersTable.id));
* return { users, orders };
* },
* (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
* const threshold = 500; // ms baseline for this resolver
*
* if (totalDbExecutionTime > threshold * 1.5) {
* console.warn(`[Performance Warning] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
* await printQueriesWithPlan(); // Analyze and print query execution plans
* } else if (totalDbExecutionTime > threshold) {
* console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
* }
*
* console.log(`DB response size: ${totalResponseSize} bytes`);
* }
* );
* ```
*
* @example
* ```typescript
* // Resolver with performance monitoring
* resolver.define("fetch", async (req: Request) => {
* try {
* return await forgeSQL.executeWithMetadata(
* async () => {
* // Resolver logic with multiple queries
* const users = await forgeSQL.selectFrom(demoUsers);
* const orders = await forgeSQL.selectFrom(demoOrders)
* .where(eq(demoOrders.userId, demoUsers.id));
* return { users, orders };
* },
* async (totalDbExecutionTime, totalResponseSize, printQueriesWithPlan) => {
* const threshold = 500; // ms baseline for this resolver
*
* if (totalDbExecutionTime > threshold * 1.5) {
* console.warn(`[Performance Warning fetch] Resolver exceeded DB time: ${totalDbExecutionTime} ms`);
* await printQueriesWithPlan(); // Optionally log or capture diagnostics for further analysis
* } else if (totalDbExecutionTime > threshold) {
* console.debug(`[Performance Debug] High DB time: ${totalDbExecutionTime} ms`);
* }
*
* console.log(`DB response size: ${totalResponseSize} bytes`);
* }
* );
* } catch (e) {
* const error = e?.cause?.debug?.sqlMessage ?? e?.cause;
* console.error(error, e);
* throw error;
* }
* });
* ```
*
* @note **Important**: When multiple resolvers are running concurrently, their query data may also appear in `printQueriesWithPlan()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
*/
async executeWithMetadata<T>(
query: () => Promise<T>,
onMetadata: (
totalDbExecutionTime: number,
totalResponseSize: number,
printQueriesWithPlan: () => Promise<void>,
) => Promise<void> | void,
options?: MetadataQueryOptions,
): Promise<T> {
return this.ormInstance.executeWithMetadata(query, onMetadata, options);
}
selectCacheable<TSelection extends SelectedFields>(
fields: TSelection,
cacheTTL?: number,
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
return this.ormInstance.selectCacheable(fields, cacheTTL);
}
selectDistinctCacheable<TSelection extends SelectedFields>(
fields: TSelection,
cacheTTL?: number,
): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {
return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
}
/**
* Creates a select query builder for all columns from a table with field aliasing support.
* This is a convenience method that automatically selects all columns from the specified table.
*
* @template T - The type of the table
* @param table - The table to select from
* @returns Select query builder with all table columns and field aliasing support
* @example
* ```typescript
* const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
* ```
*/
selectFrom<T extends MySqlTable>(table: T) {
return this.ormInstance.getDrizzleQueryBuilder().selectFrom(table);
}
/**
* Creates a select distinct query builder for all columns from a table with field aliasing support.
* This is a convenience method that automatically selects all distinct columns from the specified table.
*
* @template T - The type of the table
* @param table - The table to select from
* @returns Select distinct query builder with all table columns and field aliasing support
* @example
* ```typescript
* const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
* ```
*/
selectDistinctFrom<T extends MySqlTable>(table: T) {
return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFrom(table);
}
/**
* Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
* This is a convenience method that automatically selects all columns from the specified table with caching enabled.
*
* @template T - The type of the table
* @param table - The table to select from
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
* @returns Select query builder with all table columns, field aliasing, and caching support
* @example
* ```typescript
* const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
* ```
*/
selectCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
return this.ormInstance.getDrizzleQueryBuilder().selectFromCacheable(table, cacheTTL);
}
/**
* Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
* This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
*
* @template T - The type of the table
* @param table - The table to select from
* @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
* @returns Select distinct query builder with all table columns, field aliasing, and caching support
* @example
* ```typescript
* const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
* ```
*/
selectDistinctCacheableFrom<T extends MySqlTable>(table: T, cacheTTL?: number) {
return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFromCacheable(table, cacheTTL);
}
executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void> {
return this.ormInstance.executeWithCacheContext(cacheContext);
}
executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
}
/**
* Executes operations within a local cache context.
* This provides in-memory caching for select queries within a single request scope.
*
* @param cacheContext - Function containing operations that will benefit from local caching
* @returns Promise that resolves when all operations are complete
*/
executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void> {
return this.ormInstance.executeWithLocalContext(cacheContext);
}
/**
* Executes operations within a local cache context and returns a value.
* This provides in-memory caching for select queries within a single request scope.
*
* @param cacheContext - Function containing operations that will benefit from local caching
* @returns Promise that resolves to the return value of the cacheContext function
*/
executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T> {
return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
}
/**
* Creates an insert query builder.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
*
* @param table - The table to insert into
* @returns Insert query builder (no versioning, no cache management)
*/
insert<TTable extends MySqlTable>(
table: TTable,
): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.ormInstance.insert(table);
}
/**
* Creates an insert query builder that automatically evicts cache after execution.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
*
* @param table - The table to insert into
* @returns Insert query builder with automatic cache eviction (no versioning)
*/
insertAndEvictCache<TTable extends MySqlTable>(
table: TTable,
): MySqlInsertBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.ormInstance.insertAndEvictCache(table);
}
/**
* Creates an update query builder.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
*
* @param table - The table to update
* @returns Update query builder (no versioning, no cache management)
*/
update<TTable extends MySqlTable>(
table: TTable,
): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.ormInstance.update(table);
}
/**
* Creates an update query builder that automatically evicts cache after execution.
*
* ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
* For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
*
* @param table - The table to update
* @returns Update query builder with automatic cache eviction (no versioning)
*/
updateAndEvictCache<TTable extends MySqlTable>(
table: TTable,
): MySqlUpdateBuilder<TTable, MySqlRemoteQueryResultHKT, MySqlRemotePreparedQueryHKT> {
return this.ormInstance.updateAndEvictCache(table);
}
/**
* Creates a delete query bui