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,005 lines • 58.3 kB
TypeScript
import { Result, UpdateQueryResponse } from "@forge/sql";
import { SqlParameters } from "@forge/sql/out/sql-statement";
import { AnyMySqlSelectQueryBuilder, AnyMySqlTable, MySqlSelectBuilder, MySqlTable, MySqlColumn } from "drizzle-orm/mysql-core";
import { MySqlSelectDynamic, type SelectedFields } from "drizzle-orm/mysql-core/query-builders/select.types";
import { InferInsertModel, Query, SQL } from "drizzle-orm";
import { MySqlRemoteDatabase, MySqlRemotePreparedQueryHKT, MySqlRemoteQueryResultHKT } from "drizzle-orm/mysql-proxy";
import { SqlHints } from "../utils/sqlHints";
import { ClusterStatementRowCamelCase, ExplainAnalyzeRow, SlowQueryNormalized } from "./SystemTables";
import { ForgeSQLCacheOperations } from "./ForgeSQLCacheOperations";
import { DeleteAndEvictCacheType, ExecuteQuery, ExecuteQueryCacheable, InsertAndEvictCacheType, SelectAliasedCacheableType, SelectAliasedDistinctCacheableType, SelectAliasedDistinctType, SelectAliasedType, SelectAllDistinctFromAliasedType, SelectAllDistinctFromCacheableAliasedType, SelectAllFromAliasedType, SelectAllFromCacheableAliasedType, UpdateAndEvictCacheType } from "..";
import { MySqlDeleteBase, MySqlInsertBuilder, MySqlSelectBase, MySqlUpdateBuilder } from "drizzle-orm/mysql-core/query-builders";
import { GetSelectTableName, GetSelectTableSelection, SelectResultField } from "drizzle-orm/query-builders/select.types";
import { SQLWrapper } from "drizzle-orm/sql/sql";
import type { MySqlQueryResultKind } from "drizzle-orm/mysql-core/session";
import type { WithBuilder } from "drizzle-orm/mysql-core/subquery";
import { WithSubquery } from "drizzle-orm/subquery";
import { MetadataQueryOptions } from "../utils/metadataContextUtils";
/**
* Core interface for ForgeSQL operations.
* Provides access to CRUD operations, schema-level SQL operations, and query analysis capabilities.
*
* This is the main interface that developers interact with when using ForgeSQL ORM.
* It combines query building capabilities with database operations and caching.
*
* @interface ForgeSqlOperation
* @extends {QueryBuilderForgeSql}
*/
export interface ForgeSqlOperation extends QueryBuilderForgeSql {
/**
* Creates a new query builder for the given entity.
* @returns {MySqlRemoteDatabase<Record<string, unknown>>} The Drizzle database instance for building queries
*/
getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
selectAliased: SelectAliasedType;
selectAliasedDistinct: SelectAliasedDistinctType;
executeQuery: ExecuteQuery;
selectAliasedCacheable: SelectAliasedCacheableType;
selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
executeQueryCacheable: ExecuteQueryCacheable;
insertWithCacheContext: InsertAndEvictCacheType;
insertAndEvictCache: InsertAndEvictCacheType;
updateAndEvictCache: UpdateAndEvictCacheType;
updateWithCacheContext: UpdateAndEvictCacheType;
deleteAndEvictCache: DeleteAndEvictCacheType;
deleteWithCacheContext: DeleteAndEvictCacheType;
selectFrom: SelectAllFromAliasedType;
selectDistinctFrom: SelectAllDistinctFromAliasedType;
selectFromCacheable: SelectAllFromCacheableAliasedType;
selectDistinctFromCacheable: SelectAllDistinctFromCacheableAliasedType;
};
/**
* Provides modify (Create, Update, Delete) operations with optimistic locking support.
* @returns {VerioningModificationForgeSQL} Interface for performing CRUD operations
*/
modifyWithVersioning(): VerioningModificationForgeSQL;
/**
* Provides schema-level SQL fetch operations with type safety.
* @returns {SchemaSqlForgeSql} Interface for executing schema-bound SQL queries
*/
fetch(): SchemaSqlForgeSql;
/**
* Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
* @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
*/
analyze(): SchemaAnalyzeForgeSql;
/**
* 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;
/**
* 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;
}
/**
* Interface for Query Builder operations.
* Provides access to the underlying Drizzle ORM query builder with enhanced functionality.
*
* This interface extends Drizzle's query building capabilities with:
* - Field aliasing to prevent name collisions in joins
* - Caching support for select operations
* - Automatic cache eviction for modify operations
*
* @interface QueryBuilderForgeSql
*/
export interface QueryBuilderForgeSql {
/**
* Creates a new query builder for the given entity.
* @returns {MySqlRemoteDatabase<Record<string, unknown>>} The Drizzle database instance for building queries
*/
getDrizzleQueryBuilder(): MySqlRemoteDatabase<Record<string, unknown>> & {
selectAliased: SelectAliasedType;
selectAliasedDistinct: SelectAliasedDistinctType;
executeQuery: ExecuteQuery;
selectAliasedCacheable: SelectAliasedCacheableType;
selectAliasedDistinctCacheable: SelectAliasedDistinctCacheableType;
executeQueryCacheable: ExecuteQueryCacheable;
insertWithCacheContext: InsertAndEvictCacheType;
insertAndEvictCache: InsertAndEvictCacheType;
updateAndEvictCache: UpdateAndEvictCacheType;
updateWithCacheContext: UpdateAndEvictCacheType;
deleteAndEvictCache: DeleteAndEvictCacheType;
deleteWithCacheContext: DeleteAndEvictCacheType;
};
/**
* 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, MySqlRemotePreparedQueryHKT>} 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>;
/**
* 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): MySqlSelectBase<GetSelectTableName<T>, GetSelectTableSelection<T>, "single", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, {
[K in keyof {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}]: {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}[K];
}[], any>;
/**
* 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, MySqlRemotePreparedQueryHKT>} 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>;
/**
* 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): MySqlSelectBase<GetSelectTableName<T>, GetSelectTableSelection<T>, "single", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, {
[K in keyof {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}]: {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}[K];
}[], any>;
/**
* 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>;
/**
* 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): MySqlSelectBase<GetSelectTableName<T>, GetSelectTableSelection<T>, "single", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, {
[K in keyof {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}]: {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}[K];
}[], any>;
/**
* 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>;
/**
* 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): MySqlSelectBase<GetSelectTableName<T>, GetSelectTableSelection<T>, "single", MySqlRemotePreparedQueryHKT, GetSelectTableName<T> extends string ? Record<string & GetSelectTableName<T>, "not-null"> : {}, false, never, {
[K in keyof {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}]: {
[Key in keyof GetSelectTableSelection<T>]: SelectResultField<GetSelectTableSelection<T>[Key]>;
}[K];
}[], any>;
/**
* 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>;
/**
* 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>;
/**
* 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>;
/**
* 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>;
/**
* 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>;
/**
* 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>;
/**
* 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.
*
* @param cacheContext - Function containing operations that may trigger cache evictions
* @returns Promise that resolves when all operations and cache clearing are complete
*/
executeWithCacheContext(cacheContext: () => Promise<void>): Promise<void>;
/**
* 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
*/
executeWithCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T>;
/**
* Executes operations within a local cache context that provides in-memory caching for select queries.
* This is useful for optimizing queries within a single resolver or request scope.
*
* Local cache features:
* - Caches select query results in memory for the duration of the context
* - Automatically evicts cache when insert/update/delete operations are performed
* - Provides faster access to repeated queries within the same context
* - Does not persist across different requests or contexts
*
* @param cacheContext - Function containing operations that will benefit from local caching
* @returns Promise that resolves when all operations are complete
*
* @example
* ```typescript
* await forgeSQL.executeWithLocalContext(async () => {
* // First call - executes query and caches result
* const users = await forgeSQL.select({ id: users.id, name: users.name })
* .from(users).where(eq(users.active, true));
*
* // Second call - gets result from local cache (no database query)
* const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
* .from(users).where(eq(users.active, true));
*
* // Insert operation - evicts local cache
* await forgeSQL.insert(users).values({ name: 'New User', active: true });
* });
* ```
*/
executeWithLocalContext(cacheContext: () => Promise<void>): Promise<void>;
/**
* Executes operations within a local cache context and returns a value.
* This is useful for optimizing queries within a single resolver or request scope.
*
* Local cache features:
* - Caches select query results in memory for the duration of the context
* - Automatically evicts cache when insert/update/delete operations are performed
* - Provides faster access to repeated queries within the same context
* - Does not persist across different requests or contexts
*
* @param cacheContext - Function containing operations that will benefit from local caching
* @returns Promise that resolves to the return value of the cacheContext function
*
* @example
* ```typescript
* const result = await forgeSQL.executeWithLocalCacheContextAndReturnValue(async () => {
* // First call - executes query and caches result
* const users = await forgeSQL.select({ id: users.id, name: users.name })
* .from(users).where(eq(users.active, true));
*
* // Second call - gets result from local cache (no database query)
* const cachedUsers = await forgeSQL.select({ id: users.id, name: users.name })
* .from(users).where(eq(users.active, true));
*
* return { users, cachedUsers };
* });
* ```
*/
executeWithLocalCacheContextAndReturnValue<T>(cacheContext: () => Promise<T>): Promise<T>;
/**
* 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, printQueries) => {
* 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 printQueries(); // 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 `printQueries()` analysis, as it queries the global `CLUSTER_STATEMENTS_SUMMARY` table.
*/
executeWithMetadata<T>(query: () => Promise<T>, onMetadata: (totalDbExecutionTime: number, totalResponseSize: number, printQueriesWithPlan: () => Promise<void>) => Promise<void> | void, options?: MetadataQueryOptions): Promise<T>;
/**
* 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): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>>;
/**
* 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");
* ```
*/
executeDDL<T>(query: SQLWrapper | string): Promise<MySqlQueryResultKind<MySqlRemoteQueryResultHKT, T>>;
/**
* 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);
* }
* ```
*/
executeDDLActions<T>(actions: () => Promise<T>): Promise<T>;
/**
* 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>>;
/**
* 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)
* );
* ```
*/
$with: WithBuilder;
/**
* 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[]): {
select: {
(): MySqlSelectBuilder<undefined, MySqlRemotePreparedQueryHKT>;
<TSelection extends SelectedFields>(fields: TSelection): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
};
selectDistinct: {
(): MySqlSelectBuilder<undefined, MySqlRemotePreparedQueryHKT>;
<TSelection extends SelectedFields>(fields: TSelection): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;
};
};
}
/**
* Interface for Modify (Create, Update, Delete) operations.
* Provides methods for basic database operations with support for optimistic locking.
*
* @interface VerioningModificationForgeSQL
*/
export interface VerioningModificationForgeSQL {
/**
* Inserts multiple records into the database.
* @template T - The type of the table schema
* @param {T} schema - The entity schema
* @param {InferInsertModel<T>[]} models - The list of entities to insert
* @param {boolean} [updateIfExists] - Whether to update the row if it already exists (default: false)
* @returns {Promise<number>} The number of inserted rows
* @throws {Error} If the insert operation fails
*/
insert<T extends AnyMySqlTable>(schema: T, models: InferInsertModel<T>[], updateIfExists?: boolean): Promise<number>;
/**
* Deletes a record by its ID.
* @template T - The type of the table schema
* @param {unknown} id - The ID of the record to delete
* @param {T} schema - The entity schema
* @returns {Promise<number>} The number of rows affected
* @throws {Error} If the delete operation fails
*/
deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number>;
/**
* Updates a record by its ID with optimistic locking support.
* If a version field is defined in the schema, versioning is applied:
* - the current record version is retrieved
* - checked for concurrent modifications
* - and then incremented
*
* @template T - The type of the table schema
* @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
* @param {T} schema - The entity schema
* @returns {Promise<number>} The number of rows affected
* @throws {Error} If the primary key is not included in the update fields
* @throws {Error} If optimistic locking check fails
*/
updateById<T extends AnyMySqlTable>(entity: Partial<InferInsertModel<T>>, schema: T): Promise<number>;
/**
* Updates specified fields of records based on provided conditions.
* If the "where" parameter is not provided, the WHERE clause is built from the entity fields
* that are not included in the list of fields to update.
*
* @template T - The type of the table schema
* @param {Partial<InferInsertModel<T>>} updateData - The object containing values to update
* @param {T} schema - The entity schema
* @param {SQL<unknown>} [where] - Optional filtering conditions for the WHERE clause
* @returns {Promise<number>} The number of affected rows
* @throws {Error} If no filtering criteria are provided
* @throws {Error} If the update operation fails
*/
updateFields<T extends AnyMySqlTable>(updateData: Partial<InferInsertModel<T>>, schema: T, where?: SQL<unknown>): Promise<number>;
}
export interface CacheForgeSQL extends VerioningModificationForgeSQL {
evictCache(tables: string[]): Promise<void>;
evictCacheEntities(tables: AnyMySqlTable[]): Promise<void>;
}
/**
* Interface for schema analysis operations.
* Provides methods for analyzing query performance and execution plans.
*
* @interface SchemaAnalyzeForgeSql
*/
export interface SchemaAnalyzeForgeSql {
/**
* Executes EXPLAIN on a Drizzle query.
* @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
*/
explain(query: {
toSQL: () => Query;
}): Promise<ExplainAnalyzeRow[]>;
/**
* Executes EXPLAIN on a raw SQL query.
* @param {string} query - The SQL query to analyze
* @param {unknown[]} bindParams - The query parameters
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
*/
explainRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]>;
/**
* Executes EXPLAIN ANALYZE on a Drizzle query.
* @param {{ toSQL: () => Query }} query - The Drizzle query to analyze
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
*/
explainAnalyze(query: {
toSQL: () => Query;
}): Promise<ExplainAnalyzeRow[]>;
/**
* Executes EXPLAIN ANALYZE on a raw SQL query.
* @param {string} query - The SQL query to analyze
* @param {unknown[]} bindParams - The query parameters
* @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results
*/
explainAnalyzeRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]>;
/**
* Analyzes slow queries from the database.
* @returns {Promise<SlowQueryNormalized[]>} The normalized slow query data
*/
analyzeSlowQueries(): Promise<SlowQueryNormalized[]>;
/**
* Analyzes query history for specific tables using Drizzle table objects.
* @param {AnyMySqlTable[]} tables - The Drizzle table objects to analyze
* @param {Date} [fromDate] - The start date for the analysis
* @param {Date} [toDate] - The end date for the analysis
* @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
*/
analyzeQueriesHistory(tables: AnyMySqlTable[], fromDate?: Date, toDate?: Date): Promise<ClusterStatementRowCamelCase[]>;
/**
* Analyzes query history for specific tables using raw table names.
* @param {string[]} tables - The table names to analyze
* @param {Date} [fromDate] - The start date for the analysis
* @param {Date} [toDate] - The end date for the analysis
* @returns {Promise<ClusterStatementRowCamelCase[]>} The analyzed query history
*/
analyzeQueriesHistoryRaw(tables: string[], fromDate?: Date, toDate?: Date): Promise<ClusterStatementRowCamelCase[]>;
}
/**
* Interface for schema-level SQL operations.
* Provides methods for executing SQL queries with schema binding and type safety.
*
* @interface SchemaSqlForgeSql
*/
export interface SchemaSqlForgeSql {
/**
* Executes a Drizzle query and returns a single result.
* @template T - The type of the query builder
* @param {T} query - The Drizzle query to execute
* @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined
* @throws {Error} If more than one record is returned
* @throws {Error} If the query execution fails
*/
executeQueryOnlyOne<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(query: T): Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>;
/**
* Executes a raw SQL query and returns the results.
* @template T - The type of the result objects
* @param {string} query - The raw SQL query
* @param {SqlParameters[]} [params] - Optional SQL parameters
* @returns {Promise<T[]>} A list of results as objects
* @throws {Error} If the query execution fails
*/
executeRawSQL<T>(query: string, params?: SqlParameters[]): Promise<T[]>;
/**
* Executes a raw SQL update query.
*
* @param query - The raw SQL update query
* @param params - Optional SQL parameters
* @returns Promise that resolves to the update response containing affected rows
* @throws Error if the update operation fails
*/
executeRawUpdateSQL(query: string, params?: unknown[]): Promise<UpdateQueryResponse>;
}
/**
* Interface for Rovo integration settings.
* Defines configuration for secure dynamic SQL query execution.
*
* @interface RovoIntegrationSetting
*/
export interface RovoIntegrationSetting {
/**
* Gets the account ID of the active user.
*
* @returns {string} The account ID of the active user
*/
getActiveUser(): string;
/**
* Gets the context parameters for query substitution.
*
* @returns {Record<string, string>} Map of parameter names to their values
*/
getParameters(): Record<string, string>;
/**
* Gets the name of the table to query.
*
* @returns {string} The table name
*/
getTableName(): string;
/**
* Checks if Row-Level Security is enabled.
*
* @returns {boolean} True if RLS is enabled, false otherwise
*/
isUseRLS(): boolean;
/**
* Generates the WHERE clause for Row-Level Security filtering.
*
* @param {string} alias - The table alias to use in the WHERE clause
* @returns {string} SQL WHERE clause condition for RLS filtering
*/
userScopeWhere(alias: string): string;
/**
* Gets the list of field names required for RLS validation.
*
* @returns {string[]} Array of field names that must be present in SELECT clause for RLS
*/
userScopeFields(): string[];
}
/**
* Interface for configuring Row-Level Security (RLS) settings.
* Provides a fluent API for setting up RLS conditions, required columns, and WHERE clauses.
*
* @interface RlsSettings
*/
export interface RlsSettings {
/**
* Sets a conditional function to determine if RLS should be applied.
*
* @param {() => Promise<boolean>} condition - Async function that returns true if RLS should be enabled
* @returns {RlsSettings} This builder instance for method chaining
*/
addRlsCondition(condition: () => Promise<boolean>): RlsSettings;
/**
* Adds a column name that must be present in the SELECT clause for RLS validation.
*
* @param {string} columnName - The name of the column to require
* @returns {RlsSettings} This builder instance for method chaining
*/
addRlsColumnName(columnName: string): RlsSettings;
/**
* Adds a Drizzle column that must be present in the SELECT clause for RLS validation.
*
* @param {MySqlColumn} column - The Drizzle column object
* @returns {RlsSettings} This builder instance for method chaining
*/
addRlsColumn(columnName: MySqlColumn): RlsSettings;
/**
* Sets the WHERE clause function for RLS filtering.
*
* @param {(alias: string) => string} wherePart - Function that generates WHERE clause
* @returns {RlsSettings} This builder instance for method chaining
*/
addRlsWherePart(wherePart: (alias: string) => string): RlsSettings;
/**
* Finishes RLS configuration and returns to the settings builder.
*
* @returns {RovoIntegrationSettingCreator} The parent settings builder
*/
finish(): RovoIntegrationSettingCreator;
}
/**
* Interface for building Rovo integration settings.
* Provides a fluent API for configuring query settings including context parameters and RLS.
*
* @interface RovoIntegrationSettingCreator
*/
export interface RovoIntegrationSettingCreator {
/**
* Adds a string context parameter for query substitution.
* The value will be wrapped in single quotes in the SQL query.
*
* @param {string} parameterName - The parameter name to replace in the query (e.g., '{{projectKey}}')
* @param {string} value - The string value to substitute for the parameter
* @returns {RovoIntegrationSettingCreator} This builder instance for method chaining
*
* @example
* ```typescript
* builder.addStringContextParameter('{{projectKey}}', 'PROJ-123');
* // In SQL: {{projectKey}} will be replaced with 'PROJ-123'
* ```
*/
addStringContextParameter(parameterName: string, value: string): RovoIntegrationSettingCreator;
/**
* Adds a number context parameter for query substitution.
* The value will be inserted as-is without quotes in the SQL query.
*
* @param {string} parameterName - The parameter name to replace in the query (e.g., '{{limit}}')
* @param {number} value - The numeric value to substitute for the parameter
* @returns {RovoIntegrationSettingCreator} This builder instance for method chaining
*
* @example
* ```typescript
* builder.addNumberContextParameter('{{limit}}', 100);
* // In SQL: {{limit}} will be replaced with 100
* ```
*/
addNumberContextParameter(parameterName: string, value: number): RovoIntegrationSettingCreator;
/**
* Adds a boolean context parameter for query substitution.
* The value will be converted to 1 (true) or 0 (false) and inserted as a number.
*
* @param {string} parameterName - The parameter name to replace in the query (e.g., '{{isActive}}')
* @param {boolean} value - The boolean value to substitute for the parameter
* @returns {RovoIntegrationSettingCreator} This builder instance for method chaining
*
* @example
* ```typescript
* builder.addBooleanContextParameter('{{isActive}}', true);
* // In SQL: {{isActive}} will be replaced with 1
* ```
*/
addBooleanContextParameter(parameterName: string, value: boolean): RovoIntegrationSettingCreator;
/**
* Adds a context parameter for query substitution.
* Context parameters are replaced in the SQL query before execution.
*
* @param {string} parameterName - The parameter name to replace in the query (e.g., '{{projectKey}}')
* @param {string} value - The value to substitute for the parameter
* @param {boolean} wrap - Whether to wrap the value in single quotes (true for strings, false for numbers)
* @returns {RovoIntegrationSettingCreator} This builder instance for method chaining
*
* @example
* ```typescript
* builder.addContextParameter('{{projectKey}}', 'PROJ-123', true);
* // In SQL: {{projectKey}} will be replaced with 'PROJ-123'
* ```
*/
addContextParameter(parameterName: string, value: string, wrap: boolean): RovoIntegrationSettingCreator;
/**
* Enables Row-Level Security (RLS) for the query.
*
* @returns {RlsSettings} RLS settings builder for configuring security options
*/
useRLS(): RlsSettings;
/**
* Builds and returns the RovoIntegrationSetting instance.
*
* @returns {Promise<RovoIntegrationSetting>} The configured RovoIntegrationSetting instance
*/
build(): Promise<RovoIntegrationSetting>;
}
/**
* Interface for Rovo integration - a secure pattern for natural-language analytics.
*
* Rovo provides secure execution of dynamic SQL queries with comprehensive security validations.
*
* @interface RovoIntegration
*/
export interface RovoIntegration {
/**
* Creates a