forge-sql-orm
Version:
Drizzle ORM integration for Forge-SQL in Atlassian Forge applications.
1 lines • 122 kB
Source Map (JSON)
{"version":3,"file":"ForgeSQLORM.mjs","sources":["../src/utils/sqlUtils.ts","../src/core/ForgeSQLCrudOperations.ts","../src/core/ForgeSQLSelectOperations.ts","../src/utils/forgeDriver.ts","../src/utils/sqlHints.ts","../src/utils/forgeDriverProxy.ts","../src/lib/drizzle/extensions/selectAliased.ts","../src/core/ForgeSQLAnalyseOperations.ts","../src/core/ForgeSQLORM.ts","../src/core/ForgeSQLQueryBuilder.ts","../src/core/SystemTables.ts","../src/webtriggers/dropMigrationWebTrigger.ts","../src/webtriggers/applyMigrationsWebTrigger.ts","../src/webtriggers/fetchSchemaWebTrigger.ts","../src/webtriggers/index.ts"],"sourcesContent":["import moment from \"moment\";\nimport { AnyColumn, Column, isTable, SQL, sql, StringChunk } from \"drizzle-orm\";\nimport { AnyMySqlTable, MySqlCustomColumn } from \"drizzle-orm/mysql-core/index\";\nimport { PrimaryKeyBuilder } from \"drizzle-orm/mysql-core/primary-keys\";\nimport { AnyIndexBuilder } from \"drizzle-orm/mysql-core/indexes\";\nimport { CheckBuilder } from \"drizzle-orm/mysql-core/checks\";\nimport { ForeignKeyBuilder } from \"drizzle-orm/mysql-core/foreign-keys\";\nimport { UniqueConstraintBuilder } from \"drizzle-orm/mysql-core/unique-constraint\";\nimport type { SelectedFields } from \"drizzle-orm/mysql-core/query-builders/select.types\";\nimport { MySqlTable } from \"drizzle-orm/mysql-core\";\nimport { isSQLWrapper } from \"drizzle-orm/sql/sql\";\n\n/**\n * Interface representing table metadata information\n */\nexport interface MetadataInfo {\n /** The name of the table */\n tableName: string;\n /** Record of column names and their corresponding column definitions */\n columns: Record<string, AnyColumn>;\n /** Array of index builders */\n indexes: AnyIndexBuilder[];\n /** Array of check constraint builders */\n checks: CheckBuilder[];\n /** Array of foreign key builders */\n foreignKeys: ForeignKeyBuilder[];\n /** Array of primary key builders */\n primaryKeys: PrimaryKeyBuilder[];\n /** Array of unique constraint builders */\n uniqueConstraints: UniqueConstraintBuilder[];\n /** Array of all extra builders */\n extras: any[];\n}\n\n/**\n * Interface for config builder data\n */\ninterface ConfigBuilderData {\n value?: any;\n [key: string]: any;\n}\n\n/**\n * Parses a date string into a Date object using the specified format\n * @param value - The date string to parse\n * @param format - The format to use for parsing\n * @returns Date object\n */\nexport const parseDateTime = (value: string, format: string): Date => {\n let result: Date;\n const m = moment(value, format, true);\n if (!m.isValid()) {\n const momentDate = moment(value);\n if (momentDate.isValid()) {\n result = momentDate.toDate();\n } else {\n result = new Date(value);\n }\n } else {\n result = m.toDate();\n }\n if (isNaN(result.getTime())) {\n result = new Date(value);\n }\n return result;\n};\n\n/**\n * Gets primary keys from the schema.\n * @template T - The type of the table schema\n * @param {T} table - The table schema\n * @returns {[string, AnyColumn][]} Array of primary key name and column pairs\n */\nexport function getPrimaryKeys<T extends AnyMySqlTable>(table: T): [string, AnyColumn][] {\n const { columns, primaryKeys } = getTableMetadata(table);\n\n // First try to find primary keys in columns\n const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary) as [\n string,\n AnyColumn,\n ][];\n\n if (columnPrimaryKeys.length > 0) {\n return columnPrimaryKeys;\n }\n\n // If no primary keys found in columns, check primary key builders\n if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {\n // Collect all primary key columns from all primary key builders\n const primaryKeyColumns = new Set<[string, AnyColumn]>();\n\n primaryKeys.forEach((primaryKeyBuilder) => {\n // Get primary key columns from each builder\n Object.entries(columns)\n .filter(([, column]) => {\n // @ts-ignore - PrimaryKeyBuilder has internal columns property\n return primaryKeyBuilder.columns.includes(column);\n })\n .forEach(([name, column]) => {\n primaryKeyColumns.add([name, column]);\n });\n });\n\n return Array.from(primaryKeyColumns);\n }\n\n return [];\n}\n\n/**\n * Processes foreign keys from both foreignKeysSymbol and extraSymbol\n * @param table - The table schema\n * @param foreignKeysSymbol - Symbol for foreign keys\n * @param extraSymbol - Symbol for extra configuration\n * @returns Array of foreign key builders\n */\nfunction processForeignKeys(\n table: AnyMySqlTable,\n foreignKeysSymbol: symbol | undefined,\n extraSymbol: symbol | undefined,\n): ForeignKeyBuilder[] {\n const foreignKeys: ForeignKeyBuilder[] = [];\n\n // Process foreign keys from foreignKeysSymbol\n if (foreignKeysSymbol) {\n // @ts-ignore\n const fkArray: any[] = table[foreignKeysSymbol];\n if (fkArray) {\n fkArray.forEach((fk) => {\n if (fk.reference) {\n const item = fk.reference(fk);\n foreignKeys.push(item);\n }\n });\n }\n }\n\n // Process foreign keys from extraSymbol\n if (extraSymbol) {\n // @ts-ignore\n const extraConfigBuilder = table[extraSymbol];\n if (extraConfigBuilder && typeof extraConfigBuilder === \"function\") {\n const configBuilderData = extraConfigBuilder(table);\n if (configBuilderData) {\n const configBuilders = Array.isArray(configBuilderData)\n ? configBuilderData\n : Object.values(configBuilderData).map(\n (item) => (item as ConfigBuilderData).value ?? item,\n );\n\n configBuilders.forEach((builder) => {\n if (!builder?.constructor) return;\n\n const builderName = builder.constructor.name.toLowerCase();\n if (builderName.includes(\"foreignkeybuilder\")) {\n foreignKeys.push(builder);\n }\n });\n }\n }\n }\n\n return foreignKeys;\n}\n\n/**\n * Extracts table metadata from the schema.\n * @param {AnyMySqlTable} table - The table schema\n * @returns {MetadataInfo} Object containing table metadata\n */\nexport function getTableMetadata(table: AnyMySqlTable): MetadataInfo {\n const symbols = Object.getOwnPropertySymbols(table);\n const nameSymbol = symbols.find((s) => s.toString().includes(\"Name\"));\n const columnsSymbol = symbols.find((s) => s.toString().includes(\"Columns\"));\n const foreignKeysSymbol = symbols.find((s) => s.toString().includes(\"ForeignKeys)\"));\n const extraSymbol = symbols.find((s) => s.toString().includes(\"ExtraConfigBuilder\"));\n\n // Initialize builders arrays\n const builders = {\n indexes: [] as AnyIndexBuilder[],\n checks: [] as CheckBuilder[],\n foreignKeys: [] as ForeignKeyBuilder[],\n primaryKeys: [] as PrimaryKeyBuilder[],\n uniqueConstraints: [] as UniqueConstraintBuilder[],\n extras: [] as any[],\n };\n\n // Process foreign keys\n builders.foreignKeys = processForeignKeys(table, foreignKeysSymbol, extraSymbol);\n\n // Process extra configuration if available\n if (extraSymbol) {\n // @ts-ignore\n const extraConfigBuilder = table[extraSymbol];\n if (extraConfigBuilder && typeof extraConfigBuilder === \"function\") {\n const configBuilderData = extraConfigBuilder(table);\n if (configBuilderData) {\n // Convert configBuilderData to array if it's an object\n const configBuilders = Array.isArray(configBuilderData)\n ? configBuilderData\n : Object.values(configBuilderData).map(\n (item) => (item as ConfigBuilderData).value ?? item,\n );\n\n // Process each builder\n configBuilders.forEach((builder) => {\n if (!builder?.constructor) return;\n\n const builderName = builder.constructor.name.toLowerCase();\n\n // Map builder types to their corresponding arrays\n const builderMap = {\n indexbuilder: builders.indexes,\n checkbuilder: builders.checks,\n primarykeybuilder: builders.primaryKeys,\n uniqueconstraintbuilder: builders.uniqueConstraints,\n };\n\n // Add builder to appropriate array if it matches any type\n for (const [type, array] of Object.entries(builderMap)) {\n if (builderName.includes(type)) {\n array.push(builder);\n break;\n }\n }\n\n // Always add to extras array\n builders.extras.push(builder);\n });\n }\n }\n }\n\n return {\n tableName: nameSymbol ? (table as any)[nameSymbol] : \"\",\n columns: columnsSymbol ? ((table as any)[columnsSymbol] as Record<string, AnyColumn>) : {},\n ...builders,\n };\n}\n\n/**\n * Generates SQL statements to drop tables\n * @param tables - Array of table names\n * @returns Array of SQL statements for dropping tables\n */\nexport function generateDropTableStatements(tables: string[]): string[] {\n const dropStatements: string[] = [];\n\n tables.forEach((tableName) => {\n dropStatements.push(`DROP TABLE IF EXISTS \\`${tableName}\\`;`);\n dropStatements.push(`DROP SEQUENCE IF EXISTS \\`${tableName}\\`;`);\n });\n\n return dropStatements;\n}\n\ntype AliasColumnMap = Record<string, AnyColumn>;\n\nfunction mapSelectTableToAlias(\n table: MySqlTable,\n uniqPrefix: string,\n aliasMap: AliasColumnMap,\n): any {\n const { columns, tableName } = getTableMetadata(table);\n const selectionsTableFields: Record<string, unknown> = {};\n Object.keys(columns).forEach((name) => {\n const column = columns[name] as AnyColumn;\n const uniqName = `a_${uniqPrefix}_${tableName}_${column.name}`.toLowerCase();\n const fieldAlias = sql.raw(uniqName);\n selectionsTableFields[name] = sql`${column} as \\`${fieldAlias}\\``;\n aliasMap[uniqName] = column;\n });\n return selectionsTableFields;\n}\n\nfunction isDrizzleColumn(column: any): boolean {\n return column && typeof column === \"object\" && \"table\" in column;\n}\n\nexport function mapSelectAllFieldsToAlias(\n selections: any,\n name: string,\n uniqName: string,\n fields: any,\n aliasMap: AliasColumnMap,\n): any {\n if (isTable(fields)) {\n selections[name] = mapSelectTableToAlias(fields as MySqlTable, uniqName, aliasMap);\n } else if (isDrizzleColumn(fields)) {\n const column = fields as Column;\n const uniqAliasName = `a_${uniqName}_${column.name}`.toLowerCase();\n let aliasName = sql.raw(uniqAliasName);\n selections[name] = sql`${column} as \\`${aliasName}\\``;\n aliasMap[uniqAliasName] = column;\n } else if (isSQLWrapper(fields)) {\n selections[name] = fields;\n } else {\n const innerSelections: any = {};\n Object.entries(fields).forEach(([iname, ifields]) => {\n mapSelectAllFieldsToAlias(innerSelections, iname, `${uniqName}_${iname}`, ifields, aliasMap);\n });\n selections[name] = innerSelections;\n }\n return selections;\n}\nexport function mapSelectFieldsWithAlias<TSelection extends SelectedFields>(\n fields: TSelection,\n): { selections: TSelection; aliasMap: AliasColumnMap } {\n if (!fields) {\n throw new Error(\"fields is empty\");\n }\n const aliasMap: AliasColumnMap = {};\n const selections: any = {};\n Object.entries(fields).forEach(([name, fields]) => {\n mapSelectAllFieldsToAlias(selections, name, name, fields, aliasMap);\n });\n return { selections, aliasMap };\n}\n\nfunction getAliasFromDrizzleAlias(value: unknown): string | undefined {\n const isSQL =\n value !== null && typeof value === \"object\" && isSQLWrapper(value) && \"queryChunks\" in value;\n if (isSQL) {\n const sql = value as SQL;\n const queryChunks = sql.queryChunks;\n if (queryChunks.length > 3) {\n const aliasNameChunk = queryChunks[queryChunks.length - 2];\n if (isSQLWrapper(aliasNameChunk) && \"queryChunks\" in aliasNameChunk) {\n const aliasNameChunkSql = aliasNameChunk as SQL;\n if (aliasNameChunkSql.queryChunks?.length === 1 && aliasNameChunkSql.queryChunks[0]) {\n const queryChunksStringChunc = aliasNameChunkSql.queryChunks[0];\n if (\"value\" in queryChunksStringChunc) {\n const values = (queryChunksStringChunc as StringChunk).value;\n if (values && values.length === 1) {\n return values[0];\n }\n }\n }\n }\n }\n }\n return undefined;\n}\n\nfunction transformValue(\n value: unknown,\n alias: string,\n aliasMap: Record<string, AnyColumn>,\n): unknown {\n const column = aliasMap[alias];\n if (!column) return value;\n\n let customColumn = column as MySqlCustomColumn<any>;\n // @ts-ignore\n const fromDriver = customColumn?.mapFrom;\n if (fromDriver && value !== null && value !== undefined) {\n return fromDriver(value);\n }\n return value;\n}\n\nfunction transformObject(\n obj: Record<string, unknown>,\n selections: Record<string, unknown>,\n aliasMap: Record<string, AnyColumn>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const selection = selections[key];\n const alias = getAliasFromDrizzleAlias(selection);\n if (alias && aliasMap[alias]) {\n result[key] = transformValue(value, alias, aliasMap);\n } else if (selection && typeof selection === \"object\" && !isSQLWrapper(selection)) {\n result[key] = transformObject(\n value as Record<string, unknown>,\n selection as Record<string, unknown>,\n aliasMap,\n );\n } else {\n result[key] = value;\n }\n }\n\n return result;\n}\n\nexport function applyFromDriverTransform<T, TSelection>(\n rows: T[],\n selections: TSelection,\n aliasMap: Record<string, AnyColumn>,\n): T[] {\n return rows.map((row) => {\n const transformed = transformObject(\n row as Record<string, unknown>,\n selections as Record<string, unknown>,\n aliasMap,\n ) as Record<string, unknown>;\n\n return processNullBranches(transformed) as unknown as T;\n });\n}\n\nfunction processNullBranches(obj: Record<string, unknown>): Record<string, unknown> | null {\n if (obj === null || typeof obj !== \"object\") {\n return obj;\n }\n\n // Skip built-in objects like Date, Array, etc.\n if (obj.constructor && obj.constructor.name !== \"Object\") {\n return obj;\n }\n\n const result: Record<string, unknown> = {};\n let allNull = true;\n\n for (const [key, value] of Object.entries(obj)) {\n if (value === null || value === undefined) {\n result[key] = null;\n continue;\n }\n\n if (typeof value === \"object\") {\n const processed = processNullBranches(value as Record<string, unknown>);\n result[key] = processed;\n if (processed !== null) {\n allNull = false;\n }\n } else {\n result[key] = value;\n allNull = false;\n }\n }\n\n return allNull ? null : result;\n}\n\nexport function formatLimitOffset(limitOrOffset: number): number {\n if (typeof limitOrOffset !== \"number\" || isNaN(limitOrOffset)) {\n throw new Error(\"limitOrOffset must be a valid number\");\n }\n return sql.raw(`${limitOrOffset}`) as unknown as number;\n}\n\nexport function nextVal(sequenceName: string): number {\n return sql.raw(`NEXTVAL(${sequenceName})`) as unknown as number;\n}\n","import { ForgeSqlOrmOptions } from \"..\";\nimport { CRUDForgeSQL, ForgeSqlOperation } from \"./ForgeSQLQueryBuilder\";\nimport { AnyMySqlTable } from \"drizzle-orm/mysql-core/index\";\nimport { AnyColumn, InferInsertModel } from \"drizzle-orm\";\nimport { eq, and } from \"drizzle-orm\";\nimport { SQL } from \"drizzle-orm\";\nimport { getPrimaryKeys, getTableMetadata } from \"../utils/sqlUtils\";\n\n/**\n * Class implementing CRUD operations for ForgeSQL ORM.\n * Provides methods for inserting, updating, and deleting records with support for optimistic locking.\n */\nexport class ForgeSQLCrudOperations implements CRUDForgeSQL {\n private readonly forgeOperations: ForgeSqlOperation;\n private readonly options: ForgeSqlOrmOptions;\n\n /**\n * Creates a new instance of ForgeSQLCrudOperations.\n * @param forgeSqlOperations - The ForgeSQL operations instance\n * @param options - Configuration options for the ORM\n */\n constructor(forgeSqlOperations: ForgeSqlOperation, options: ForgeSqlOrmOptions) {\n this.forgeOperations = forgeSqlOperations;\n this.options = options;\n }\n\n /**\n * Inserts records into the database with optional versioning support.\n * If a version field exists in the schema, versioning is applied.\n *\n * @template T - The type of the table schema\n * @param {T} schema - The entity schema\n * @param {Partial<InferInsertModel<T>>[]} models - Array of entities to insert\n * @param {boolean} [updateIfExists=false] - Whether to update existing records\n * @returns {Promise<number>} The number of inserted rows\n * @throws {Error} If the insert operation fails\n */\n async insert<T extends AnyMySqlTable>(\n schema: T,\n models: InferInsertModel<T>[],\n updateIfExists: boolean = false,\n ): Promise<number> {\n if (!models?.length) return 0;\n\n const { tableName, columns } = getTableMetadata(schema);\n const versionMetadata = this.validateVersionField(tableName, columns);\n\n // Prepare models with version field if needed\n const preparedModels = models.map((model) =>\n this.prepareModelWithVersion(model, versionMetadata, columns),\n );\n\n // Build insert query\n const queryBuilder = this.forgeOperations\n .getDrizzleQueryBuilder()\n .insert(schema)\n .values(preparedModels);\n\n // Add onDuplicateKeyUpdate if needed\n const finalQuery = updateIfExists\n ? queryBuilder.onDuplicateKeyUpdate({\n set: Object.fromEntries(\n Object.keys(preparedModels[0]).map((key) => [key, (schema as any)[key]]),\n ) as any,\n })\n : queryBuilder;\n\n // Execute query\n const result = await finalQuery;\n return result[0].insertId;\n }\n\n /**\n * Deletes a record by its primary key with optional version check.\n * If versioning is enabled, ensures the record hasn't been modified since last read.\n *\n * @template T - The type of the table schema\n * @param {unknown} id - The ID of the record to delete\n * @param {T} schema - The entity schema\n * @returns {Promise<number>} Number of affected rows\n * @throws {Error} If the delete operation fails\n * @throws {Error} If multiple primary keys are found\n */\n async deleteById<T extends AnyMySqlTable>(id: unknown, schema: T): Promise<number> {\n const { tableName, columns } = getTableMetadata(schema);\n const primaryKeys = this.getPrimaryKeys(schema);\n\n if (primaryKeys.length !== 1) {\n throw new Error(\"Only single primary key is supported\");\n }\n\n const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];\n const versionMetadata = this.validateVersionField(tableName, columns);\n\n // Build delete conditions\n const conditions: SQL<unknown>[] = [eq(primaryKeyColumn, id)];\n\n // Add version check if needed\n if (versionMetadata && columns) {\n const versionField = columns[versionMetadata.fieldName];\n if (versionField) {\n const oldModel = await this.getOldModel({ [primaryKeyName]: id }, schema, [\n versionMetadata.fieldName,\n versionField,\n ]);\n conditions.push(eq(versionField, (oldModel as any)[versionMetadata.fieldName]));\n }\n }\n\n // Execute delete query\n const queryBuilder = this.forgeOperations\n .getDrizzleQueryBuilder()\n .delete(schema)\n .where(and(...conditions));\n\n const result = await queryBuilder;\n\n return result[0].affectedRows;\n }\n\n /**\n * Updates a record by its primary key with optimistic locking support.\n * If versioning is enabled:\n * - Retrieves the current version\n * - Checks for concurrent modifications\n * - Increments the version on successful update\n *\n * @template T - The type of the table schema\n * @param {Partial<InferInsertModel<T>>} entity - The entity with updated values\n * @param {T} schema - The entity schema\n * @returns {Promise<number>} Number of affected rows\n * @throws {Error} If the primary key is not provided\n * @throws {Error} If optimistic locking check fails\n * @throws {Error} If multiple primary keys are found\n */\n async updateById<T extends AnyMySqlTable>(\n entity: Partial<InferInsertModel<T>>,\n schema: T,\n ): Promise<number> {\n const { tableName, columns } = getTableMetadata(schema);\n const primaryKeys = this.getPrimaryKeys(schema);\n\n if (primaryKeys.length !== 1) {\n throw new Error(\"Only single primary key is supported\");\n }\n\n const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];\n const versionMetadata = this.validateVersionField(tableName, columns);\n\n // Validate primary key\n if (!(primaryKeyName in entity)) {\n throw new Error(`Primary key ${primaryKeyName} must be provided in the entity`);\n }\n\n // Get current version if needed\n const currentVersion = await this.getCurrentVersion(\n entity,\n primaryKeyName,\n versionMetadata,\n columns,\n schema,\n );\n\n // Prepare update data with version\n const updateData = this.prepareUpdateData(entity, versionMetadata, columns, currentVersion);\n\n // Build update conditions\n const conditions: SQL<unknown>[] = [\n eq(primaryKeyColumn, entity[primaryKeyName as keyof typeof entity]),\n ];\n if (versionMetadata && columns) {\n const versionField = columns[versionMetadata.fieldName];\n if (versionField) {\n conditions.push(eq(versionField, currentVersion));\n }\n }\n\n // Execute update query\n const queryBuilder = this.forgeOperations\n .getDrizzleQueryBuilder()\n .update(schema)\n .set(updateData)\n .where(and(...conditions));\n\n const result = await queryBuilder;\n // Check optimistic locking\n if (versionMetadata && result[0].affectedRows === 0) {\n throw new Error(\n `Optimistic locking failed: record with primary key ${entity[primaryKeyName as keyof typeof entity]} has been modified`,\n );\n }\n\n return result[0].affectedRows;\n }\n\n /**\n * Updates specified fields of records based on provided conditions.\n * This method does not support versioning and should be used with caution.\n *\n * @template T - The type of the table schema\n * @param {Partial<InferInsertModel<T>>} updateData - The data to update\n * @param {T} schema - The entity schema\n * @param {SQL<unknown>} where - The WHERE conditions\n * @returns {Promise<number>} Number of affected rows\n * @throws {Error} If WHERE conditions are not provided\n * @throws {Error} If the update operation fails\n */\n async updateFields<T extends AnyMySqlTable>(\n updateData: Partial<InferInsertModel<T>>,\n schema: T,\n where?: SQL<unknown>,\n ): Promise<number> {\n if (!where) {\n throw new Error(\"WHERE conditions must be provided\");\n }\n\n const queryBuilder = this.forgeOperations\n .getDrizzleQueryBuilder()\n .update(schema)\n .set(updateData)\n .where(where);\n\n const result = await queryBuilder;\n return result[0].affectedRows;\n }\n\n // Helper methods\n\n /**\n * Gets primary keys from the schema.\n * @template T - The type of the table schema\n * @param {T} schema - The table schema\n * @returns {[string, AnyColumn][]} Array of primary key name and column pairs\n * @throws {Error} If no primary keys are found\n */\n private getPrimaryKeys<T extends AnyMySqlTable>(schema: T): [string, AnyColumn][] {\n const primaryKeys = getPrimaryKeys(schema);\n if (!primaryKeys) {\n throw new Error(`No primary keys found for schema: ${schema}`);\n }\n\n return primaryKeys;\n }\n\n /**\n * Validates and retrieves version field metadata.\n * @param {string} tableName - The name of the table\n * @param {Record<string, AnyColumn>} columns - The table columns\n * @returns {Object | undefined} Version field metadata if valid, undefined otherwise\n */\n private validateVersionField(\n tableName: string,\n columns: Record<string, AnyColumn>,\n ): { fieldName: string; type: string } | undefined {\n if (this.options.disableOptimisticLocking) {\n return undefined;\n }\n const versionMetadata = this.options.additionalMetadata?.[tableName]?.versionField;\n if (!versionMetadata) return undefined;\n let fieldName = versionMetadata.fieldName;\n\n let versionField = columns[versionMetadata.fieldName];\n if (!versionField) {\n const find = Object.entries(columns).find(([, c]) => c.name === versionMetadata.fieldName);\n if (find) {\n fieldName = find[0];\n versionField = find[1];\n }\n }\n if (!versionField) {\n console.warn(\n `Version field \"${versionMetadata.fieldName}\" not found in table ${tableName}. Versioning will be skipped.`,\n );\n return undefined;\n }\n\n if (!versionField.notNull) {\n console.warn(\n `Version field \"${versionMetadata.fieldName}\" in table ${tableName} is nullable. Versioning may not work correctly.`,\n );\n return undefined;\n }\n\n const fieldType = versionField.getSQLType();\n const isSupportedType =\n fieldType === \"datetime\" ||\n fieldType === \"timestamp\" ||\n fieldType === \"int\" ||\n fieldType === \"number\" ||\n fieldType === \"decimal\";\n\n if (!isSupportedType) {\n console.warn(\n `Version field \"${versionMetadata.fieldName}\" in table ${tableName} has unsupported type \"${fieldType}\". ` +\n `Only datetime, timestamp, int, and decimal types are supported for versioning. Versioning will be skipped.`,\n );\n return undefined;\n }\n\n return { fieldName, type: fieldType };\n }\n\n /**\n * Gets the current version of an entity.\n * @template T - The type of the table schema\n * @param {Partial<InferInsertModel<T>>} entity - The entity\n * @param {string} primaryKeyName - The name of the primary key\n * @param {Object | undefined} versionMetadata - Version field metadata\n * @param {Record<string, AnyColumn>} columns - The table columns\n * @param {T} schema - The table schema\n * @returns {Promise<unknown>} The current version value\n */\n private async getCurrentVersion<T extends AnyMySqlTable>(\n entity: Partial<InferInsertModel<T>>,\n primaryKeyName: string,\n versionMetadata: { fieldName: string; type: string } | undefined,\n columns: Record<string, AnyColumn>,\n schema: T,\n ): Promise<unknown> {\n if (!versionMetadata || !columns) return undefined;\n\n const versionField = columns[versionMetadata.fieldName];\n if (!versionField) return undefined;\n\n if (versionMetadata.fieldName in entity) {\n return entity[versionMetadata.fieldName as keyof typeof entity];\n }\n\n const oldModel = await this.getOldModel(\n { [primaryKeyName]: entity[primaryKeyName as keyof typeof entity] },\n schema,\n [versionMetadata.fieldName, versionField],\n );\n\n return (oldModel as any)[versionMetadata.fieldName];\n }\n\n /**\n * Prepares a model for insertion with version field.\n * @template T - The type of the table schema\n * @param {Partial<InferInsertModel<T>>} model - The model to prepare\n * @param {Object | undefined} versionMetadata - Version field metadata\n * @param {Record<string, AnyColumn>} columns - The table columns\n * @returns {InferInsertModel<T>} The prepared model\n */\n private prepareModelWithVersion<T extends AnyMySqlTable>(\n model: Partial<InferInsertModel<T>>,\n versionMetadata: { fieldName: string; type: string } | undefined,\n columns: Record<string, AnyColumn>,\n ): InferInsertModel<T> {\n if (!versionMetadata || !columns) return model as InferInsertModel<T>;\n let fieldName = versionMetadata.fieldName;\n let versionField = columns[versionMetadata.fieldName];\n if (!versionField) {\n const find = Object.entries(columns).find(([, c]) => c.name === versionMetadata.fieldName);\n if (find) {\n fieldName = find[0];\n versionField = find[1];\n }\n }\n\n if (!versionField) return model as InferInsertModel<T>;\n\n const modelWithVersion = { ...model };\n const fieldType = versionField.getSQLType();\n const versionValue = fieldType === \"datetime\" || fieldType === \"timestamp\" ? new Date() : 1;\n modelWithVersion[fieldName as keyof typeof modelWithVersion] = versionValue as any;\n\n return modelWithVersion as InferInsertModel<T>;\n }\n\n /**\n * Prepares update data with version field.\n * @template T - The type of the table schema\n * @param {Partial<InferInsertModel<T>>} entity - The entity to update\n * @param {Object | undefined} versionMetadata - Version field metadata\n * @param {Record<string, AnyColumn>} columns - The table columns\n * @param {unknown} currentVersion - The current version value\n * @returns {Partial<InferInsertModel<T>>} The prepared update data\n */\n private prepareUpdateData<T extends AnyMySqlTable>(\n entity: Partial<InferInsertModel<T>>,\n versionMetadata: { fieldName: string; type: string } | undefined,\n columns: Record<string, AnyColumn>,\n currentVersion: unknown,\n ): Partial<InferInsertModel<T>> {\n const updateData = { ...entity };\n\n if (versionMetadata && columns) {\n const versionField = columns[versionMetadata.fieldName];\n if (versionField) {\n const fieldType = versionField.getSQLType();\n updateData[versionMetadata.fieldName as keyof typeof updateData] =\n fieldType === \"datetime\" || fieldType === \"timestamp\"\n ? new Date()\n : (((currentVersion as number) + 1) as any);\n }\n }\n\n return updateData;\n }\n\n /**\n * Retrieves an existing model by primary key.\n * @template T - The type of the table schema\n * @param {Record<string, unknown>} primaryKeyValues - The primary key values\n * @param {T} schema - The table schema\n * @param {[string, AnyColumn]} versionField - The version field name and column\n * @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} The existing model\n * @throws {Error} If the record is not found\n */\n private async getOldModel<T extends AnyMySqlTable>(\n primaryKeyValues: Record<string, unknown>,\n schema: T,\n versionField: [string, AnyColumn],\n ): Promise<\n Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined\n > {\n const [versionFieldName, versionFieldColumn] = versionField;\n const primaryKeys = this.getPrimaryKeys(schema);\n const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];\n\n const resultQuery = this.forgeOperations\n .getDrizzleQueryBuilder()\n .select({\n [primaryKeyName]: primaryKeyColumn as any,\n [versionFieldName]: versionFieldColumn as any,\n })\n .from(schema)\n .where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));\n\n const model = await this.forgeOperations.fetch().executeQueryOnlyOne(resultQuery);\n\n if (!model) {\n throw new Error(`Record not found in table ${schema}`);\n }\n\n return model as Awaited<T> extends Array<any> ? Awaited<T>[number] : Awaited<T>;\n }\n}\n","import { sql, UpdateQueryResponse } from \"@forge/sql\";\nimport { ForgeSqlOrmOptions, SchemaSqlForgeSql } from \"./ForgeSQLQueryBuilder\";\nimport {\n AnyMySqlSelectQueryBuilder,\n MySqlSelectDynamic,\n} from \"drizzle-orm/mysql-core/query-builders/select.types\";\n\n/**\n * Class implementing SQL select operations for ForgeSQL ORM.\n * Provides methods for executing queries and mapping results to entity types.\n */\nexport class ForgeSQLSelectOperations implements SchemaSqlForgeSql {\n private readonly options: ForgeSqlOrmOptions;\n\n /**\n * Creates a new instance of ForgeSQLSelectOperations.\n * @param {ForgeSqlOrmOptions} options - Configuration options for the ORM\n */\n constructor(options: ForgeSqlOrmOptions) {\n this.options = options;\n }\n\n /**\n * Executes a Drizzle query and returns a single result.\n * Throws an error if more than one record is returned.\n *\n * @template T - The type of the query builder\n * @param {T} query - The Drizzle query to execute\n * @returns {Promise<Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined>} A single result object or undefined\n * @throws {Error} If more than one record is returned\n */\n async executeQueryOnlyOne<T extends MySqlSelectDynamic<AnyMySqlSelectQueryBuilder>>(\n query: T,\n ): Promise<\n Awaited<T> extends Array<any> ? Awaited<T>[number] | undefined : Awaited<T> | undefined\n > {\n const results: Awaited<T> = await query;\n const datas = results as unknown[];\n if (!datas.length) {\n return undefined;\n }\n if (datas.length > 1) {\n throw new Error(`Expected 1 record but returned ${datas.length}`);\n }\n\n return datas[0] as Awaited<T> extends Array<any> ? Awaited<T>[number] : Awaited<T>;\n }\n\n /**\n * Executes a raw SQL query and returns the results.\n * Logs the query if logging is enabled.\n *\n * @template T - The type of the result objects\n * @param {string} query - The raw SQL query to execute\n * @param {SqlParameters[]} [params] - Optional SQL parameters\n * @returns {Promise<T[]>} A list of results as objects\n */\n async executeRawSQL<T extends object | unknown>(query: string, params?: unknown[]): Promise<T[]> {\n if (this.options.logRawSqlQuery) {\n const paramsStr = params ? `, with params: ${JSON.stringify(params)}` : \"\";\n console.debug(`Executing with SQL ${query}${paramsStr}`);\n }\n const sqlStatement = sql.prepare<T>(query);\n if (params) {\n sqlStatement.bindParams(...params);\n }\n const result = await sqlStatement.execute();\n return result.rows as T[];\n }\n\n /**\n * Executes a raw SQL update query.\n * @param {string} query - The raw SQL update query\n * @param {SqlParameters[]} [params] - Optional SQL parameters\n * @returns {Promise<UpdateQueryResponse>} The update response containing affected rows\n */\n async executeRawUpdateSQL(query: string, params?: unknown[]): Promise<UpdateQueryResponse> {\n const sqlStatement = sql.prepare<UpdateQueryResponse>(query);\n if (params) {\n sqlStatement.bindParams(...params);\n }\n if (this.options.logRawSqlQuery) {\n console.debug(\n `Executing Update with SQL ${query}` +\n (params ? `, with params: ${JSON.stringify(params)}` : \"\"),\n );\n }\n const updateQueryResponseResults = await sqlStatement.execute();\n return updateQueryResponseResults.rows;\n }\n}\n","import { sql, UpdateQueryResponse } from \"@forge/sql\";\n\ninterface ForgeSQLResult {\n rows: Record<string, unknown>[] | Record<string, unknown>;\n}\n\nexport const forgeDriver = async (\n query: string,\n params: any[],\n method: \"all\" | \"execute\",\n): Promise<{\n rows: any[];\n insertId?: number;\n affectedRows?: number;\n}> => {\n try {\n if (method == \"execute\") {\n const sqlStatement = sql.prepare<UpdateQueryResponse>(query);\n if (params) {\n sqlStatement.bindParams(...params);\n }\n const updateQueryResponseResults = await sqlStatement.execute();\n let result = updateQueryResponseResults.rows as any;\n return { ...result, rows: [result] };\n } else {\n const sqlStatement = await sql.prepare<unknown>(query);\n if (params) {\n await sqlStatement.bindParams(...params);\n }\n const result = (await sqlStatement.execute()) as ForgeSQLResult;\n let rows;\n rows = (result.rows as any[]).map((r) => Object.values(r as Record<string, unknown>));\n return { rows: rows };\n }\n } catch (error) {\n console.error(\"SQL Error:\", JSON.stringify(error));\n throw error;\n }\n};\n","/**\n * Interface for SQL hints configuration\n */\nexport interface SqlHints {\n /** SQL hints for SELECT queries */\n select?: string[];\n /** SQL hints for INSERT queries */\n insert?: string[];\n /** SQL hints for UPDATE queries */\n update?: string[];\n /** SQL hints for DELETE queries */\n delete?: string[];\n}\n\n/**\n * Detects the type of SQL query and injects appropriate hints\n * @param query - The SQL query to analyze\n * @param hints - The hints configuration\n * @returns The modified query with injected hints\n */\nexport function injectSqlHints(query: string, hints?: SqlHints): string {\n if (!hints) {\n return query;\n }\n\n // Normalize the query for easier matching\n const normalizedQuery = query.trim().toUpperCase();\n\n // Get the appropriate hints based on query type\n let queryHints: string[] | undefined;\n\n if (normalizedQuery.startsWith(\"SELECT\")) {\n queryHints = hints.select;\n } else if (normalizedQuery.startsWith(\"INSERT\")) {\n queryHints = hints.insert;\n } else if (normalizedQuery.startsWith(\"UPDATE\")) {\n queryHints = hints.update;\n } else if (normalizedQuery.startsWith(\"DELETE\")) {\n queryHints = hints.delete;\n }\n\n // If no hints for this query type, return original query\n if (!queryHints || queryHints.length === 0) {\n return query;\n }\n\n // Join all hints with spaces\n const hintsString = queryHints.join(\" \");\n\n // Inject hints into the query\n if (normalizedQuery.startsWith(\"SELECT\")) {\n return `SELECT /*+ ${hintsString} */ ${query.substring(6)}`;\n } else if (normalizedQuery.startsWith(\"INSERT\")) {\n return `INSERT /*+ ${hintsString} */ ${query.substring(6)}`;\n } else if (normalizedQuery.startsWith(\"UPDATE\")) {\n return `UPDATE /*+ ${hintsString} */ ${query.substring(6)}`;\n } else if (normalizedQuery.startsWith(\"DELETE\")) {\n return `DELETE /*+ ${hintsString} */ ${query.substring(6)}`;\n }\n\n // If no match found, return original query\n return query;\n}\n","import { forgeDriver } from \"./forgeDriver\";\nimport { injectSqlHints, SqlHints } from \"./sqlHints\";\n\n/**\n * Creates a proxy for the forgeDriver that injects SQL hints\n * @returns A proxied version of the forgeDriver\n */\nexport function createForgeDriverProxy(options?: SqlHints, logRawSqlQuery?: boolean) {\n return async (\n query: string,\n params: any[],\n method: \"all\" | \"execute\",\n ): Promise<{\n rows: any[];\n insertId?: number;\n affectedRows?: number;\n }> => {\n // Inject SQL hints into the query\n const modifiedQuery = injectSqlHints(query, options);\n\n if (options && logRawSqlQuery && modifiedQuery !== query) {\n console.warn(\"modified query: \" + modifiedQuery);\n }\n // Call the original forgeDriver with the modified query\n return forgeDriver(modifiedQuery, params, method);\n };\n}\n","import { MySqlRemoteDatabase } from \"drizzle-orm/mysql-proxy\";\nimport type { SelectedFields } from \"drizzle-orm/mysql-core/query-builders/select.types\";\nimport { applyFromDriverTransform, mapSelectFieldsWithAlias } from \"../../..\";\nimport { MySqlSelectBuilder } from \"drizzle-orm/mysql-core\";\nimport { MySqlRemotePreparedQueryHKT } from \"drizzle-orm/mysql-proxy\";\n\nfunction createAliasedSelectBuilder<TSelection extends SelectedFields>(\n db: MySqlRemoteDatabase<any>,\n fields: TSelection,\n selectFn: (selections: any) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>,\n): MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT> {\n const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);\n const builder = selectFn(selections);\n\n const wrapBuilder = (rawBuilder: any): any => {\n return new Proxy(rawBuilder, {\n get(target, prop, receiver) {\n if (prop === \"execute\") {\n return async (...args: any[]) => {\n const rows = await target.execute(...args);\n return applyFromDriverTransform(rows, selections, aliasMap);\n };\n }\n\n if (prop === \"then\") {\n return (onfulfilled: any, onrejected: any) =>\n target.execute().then((rows: unknown[]) => {\n const transformed = applyFromDriverTransform(rows, selections, aliasMap);\n return onfulfilled?.(transformed);\n }, onrejected);\n }\n\n const value = Reflect.get(target, prop, receiver);\n\n if (typeof value === \"function\") {\n return (...args: any[]) => {\n const result = value.apply(target, args);\n\n if (typeof result === \"object\" && result !== null && \"execute\" in result) {\n return wrapBuilder(result);\n }\n\n return result;\n };\n }\n\n return value;\n },\n });\n };\n\n return wrapBuilder(builder);\n}\n\nexport function patchDbWithSelectAliased(db: MySqlRemoteDatabase<any>): MySqlRemoteDatabase<any> & {\n selectAliased: <TSelection extends SelectedFields>(\n fields: TSelection,\n ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;\n selectAliasedDistinct: <TSelection extends SelectedFields>(\n fields: TSelection,\n ) => MySqlSelectBuilder<TSelection, MySqlRemotePreparedQueryHKT>;\n} {\n db.selectAliased = function <TSelection extends SelectedFields>(fields: TSelection) {\n return createAliasedSelectBuilder(db, fields, (selections) => db.select(selections));\n };\n\n db.selectAliasedDistinct = function <TSelection extends SelectedFields>(fields: TSelection) {\n return createAliasedSelectBuilder(db, fields, (selections) => db.selectDistinct(selections));\n };\n\n return db;\n}\n","import { ForgeSqlOperation, SchemaAnalyzeForgeSql } from \"./ForgeSQLQueryBuilder\";\nimport { Query } from \"drizzle-orm\";\nimport {\n ClusterStatementRowCamelCase,\n ExplainAnalyzeRow,\n SlowQueryNormalized,\n} from \"./SystemTables\";\nimport { SqlParameters } from \"@forge/sql/out/sql-statement\";\nimport { AnyMySqlTable } from \"drizzle-orm/mysql-core/index\";\nimport { getTableName } from \"drizzle-orm/table\";\nimport moment from \"moment\";\n\n/**\n * Interface representing a row from the EXPLAIN ANALYZE output\n */\ninterface DecodedPlanRow {\n id: string;\n estRows?: string;\n estCost?: string;\n actRows?: string;\n task?: string;\n \"access object\"?: string;\n \"execution info\"?: string;\n \"operator info\"?: string;\n memory?: string;\n disk?: string;\n}\n\n/**\n * Interface representing a raw slow query row from the database\n */\ninterface SlowQueryRaw {\n Time: string;\n Txn_start_ts: number;\n User: string;\n Host: string;\n Conn_ID: number;\n DB: string;\n Query: string;\n Digest: string;\n Query_time: number;\n Compile_time: number;\n Optimize_time: number;\n Process_time: number;\n Wait_time: number;\n Parse_time: number;\n Rewrite_time: number;\n Cop_time: number;\n Cop_proc_avg: number;\n Cop_proc_max: number;\n Cop_proc_p90: number;\n Cop_proc_addr: string;\n Cop_wait_avg: number;\n Cop_wait_max: number;\n Cop_wait_p90: number;\n Cop_wait_addr: string;\n Mem_max: number;\n Disk_max: number;\n Total_keys: number;\n Process_keys: number;\n Request_count: number;\n KV_total: number;\n PD_total: number;\n Result_rows: number;\n Rocksdb_block_cache_hit_count: number;\n Rocksdb_block_read_count: number;\n Rocksdb_block_read_byte: number;\n Plan: string;\n Binary_plan: string;\n Plan_digest: string;\n}\n\n/**\n * Interface representing a row from the cluster statements table\n */\nexport interface ClusterStatementRow {\n INSTANCE: string;\n SUMMARY_BEGIN_TIME: string;\n SUMMARY_END_TIME: string;\n STMT_TYPE: string;\n SCHEMA_NAME: string;\n DIGEST: string;\n DIGEST_TEXT: string;\n TABLE_NAMES: string;\n INDEX_NAMES: string | null;\n SAMPLE_USER: string;\n EXEC_COUNT: number;\n SUM_ERRORS: number;\n SUM_WARNINGS: number;\n SUM_LATENCY: number;\n MAX_LATENCY: number;\n MIN_LATENCY: number;\n AVG_LATENCY: number;\n AVG_PARSE_LATENCY: number;\n MAX_PARSE_LATENCY: number;\n AVG_COMPILE_LATENCY: number;\n MAX_COMPILE_LATENCY: number;\n SUM_COP_TASK_NUM: number;\n MAX_COP_PROCESS_TIME: number;\n MAX_COP_PROCESS_ADDRESS: string;\n MAX_COP_WAIT_TIME: number;\n MAX_COP_WAIT_ADDRESS: string;\n AVG_PROCESS_TIME: number;\n MAX_PROCESS_TIME: number;\n AVG_WAIT_TIME: number;\n MAX_WAIT_TIME: number;\n AVG_BACKOFF_TIME: number;\n MAX_BACKOFF_TIME: number;\n AVG_TOTAL_KEYS: number;\n MAX_TOTAL_KEYS: number;\n AVG_PROCESSED_KEYS: number;\n MAX_PROCESSED_KEYS: number;\n AVG_ROCKSDB_DELETE_SKIPPED_COUNT: number;\n MAX_ROCKSDB_DELETE_SKIPPED_COUNT: number;\n AVG_ROCKSDB_KEY_SKIPPED_COUNT: number;\n MAX_ROCKSDB_KEY_SKIPPED_COUNT: number;\n AVG_ROCKSDB_BLOCK_CACHE_HIT_COUNT: number;\n MAX_ROCKSDB_BLOCK_CACHE_HIT_COUNT: number;\n AVG_ROCKSDB_BLOCK_READ_COUNT: number;\n MAX_ROCKSDB_BLOCK_READ_COUNT: number;\n AVG_ROCKSDB_BLOCK_READ_BYTE: number;\n MAX_ROCKSDB_BLOCK_READ_BYTE: number;\n AVG_PREWRITE_TIME: number;\n MAX_PREWRITE_TIME: number;\n AVG_COMMIT_TIME: number;\n MAX_COMMIT_TIME: number;\n AVG_GET_COMMIT_TS_TIME: number;\n MAX_GET_COMMIT_TS_TIME: number;\n AVG_COMMIT_BACKOFF_TIME: number;\n MAX_COMMIT_BACKOFF_TIME: number;\n AVG_RESOLVE_LOCK_TIME: number;\n MAX_RESOLVE_LOCK_TIME: number;\n AVG_LOCAL_LATCH_WAIT_TIME: number;\n MAX_LOCAL_LATCH_WAIT_TIME: number;\n AVG_WRITE_KEYS: number;\n MAX_WRITE_KEYS: number;\n AVG_WRITE_SIZE: number;\n MAX_WRITE_SIZE: number;\n AVG_PREWRITE_REGIONS: number;\n MAX_PREWRITE_REGIONS: number;\n AVG_TXN_RETRY: number;\n MAX_TXN_RETRY: number;\n SUM_EXEC_RETRY: number;\n SUM_EXEC_RETRY_TIME: number;\n SUM_BACKOFF_TIMES: number;\n BACKOFF_TYPES: string | null;\n AVG_MEM: number;\n MAX_MEM: number;\n AVG_DISK: number;\n MAX_DISK: number;\n AVG_KV_TIME: number;\n AVG_PD_TIME: number;\n AVG_BACKOFF_TOTAL_TIME: number;\n AVG_WRITE_SQL_RESP_TIME: number;\n AVG_TIDB_CPU_TIME: number;\n AVG_TIKV_CPU_TIME: number;\n MAX_RESULT_ROWS: number;\n MIN_RESULT_ROWS: number;\n AVG_RESULT_ROWS: number;\n PREPARED: number;\n AVG_AFFECTED_ROWS: number;\n FIRST_SEEN: string;\n LAST_SEEN: string;\n PLAN_IN_CACHE: number;\n PLAN_CACHE_HITS: number;\n PLAN_IN_BINDING: number;\n QUERY_SAMPLE_TEXT: string;\n PREV_SAMPLE_TEXT: string;\n PLAN_DIGEST: string;\n PLAN: string;\n BINARY_PLAN: string;\n CHARSET: string;\n COLLATION: string;\n PLAN_HINT: string;\n MAX_REQUEST_UNIT_READ: number;\n AVG_REQUEST_UNIT_READ: number;\n MAX_REQUEST_UNIT_WRITE: number;\n AVG_REQUEST_UNIT_WRITE: number;\n MAX_QUEUED_RC_TIME: number;\n AVG_QUEUED_RC_TIME: number;\n RESOURCE_GROUP: string;\n PLAN_CACHE_UNQUALIFIED: number;\n PLAN_CACHE_UNQUALIFIED_LAST_REASON: string;\n}\n\n/**\n * Class implementing SQL analysis operations for ForgeSQL ORM.\n * Provides methods for analyzing query performance, execution plans, and slow queries.\n */\nexport class ForgeSQLAnalyseOperation implements SchemaAnalyzeForgeSql {\n private readonly forgeOperations: ForgeSqlOperation;\n\n /**\n * Creates a new instance of ForgeSQLAnalizeOperation.\n * @param {ForgeSqlOperation} forgeOperations - The ForgeSQL operations instance\n */\n constructor(forgeOperations: ForgeSqlOperation) {\n this.forgeOperations = forgeOperations;\n this.mapToCamelCaseClusterStatement = this.mapToCamelCaseClusterStatement.bind(this);\n }\n\n /**\n * Executes EXPLAIN on a raw SQL query.\n * @param {string} query - The SQL query to analyze\n * @param {unknown[]} bindParams - The query parameters\n * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results\n */\n async explainRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]> {\n const results = await this.forgeOperations\n .fetch()\n .executeRawSQL<DecodedPlanRow>(`EXPLAIN ${query}`, bindParams as SqlParameters);\n return results.map((row) => ({\n id: row.id,\n estRows: row.estRows,\n actRows: row.actRows,\n task: row.task,\n accessObject: row[\"access object\"],\n executionInfo: row[\"execution info\"],\n operatorInfo: row[\"operator info\"],\n memory: row.memory,\n disk: row.disk,\n }));\n }\n\n /**\n * Executes EXPLAIN on a Drizzle query.\n * @param {{ toSQL: () => Query }} query - The Drizzle query to analyze\n * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results\n */\n async explain(query: { toSQL: () => Query }): Promise<ExplainAnalyzeRow[]> {\n const { sql, params } = query.toSQL();\n return this.explainRaw(sql, params);\n }\n\n /**\n * Executes EXPLAIN ANALYZE on a raw SQL query.\n * @param {string} query - The SQL query to analyze\n * @param {unknown[]} bindParams - The query parameters\n * @returns {Promise<ExplainAnalyzeRow[]>} The execution plan analysis results\n */\n async explainAnalyzeRaw(query: string, bindParams: unknown[]): Promise<ExplainAnalyzeRow[]> {\n const results = await this.forgeOperations\n .fetch()\n .executeRawSQL<DecodedPlanRow>(`EXPLAIN ANALYZE ${query}`, bindParams as SqlParameters);\n return results.map((row) => ({\n id: row.id,\n estRows: row.estRows,\n actRows: row.actRows,\n task: row.task,\n accessObject: row[\"access object\"],\n executionInfo: row[\"execution info\"],\n operatorInfo: row[\"operator info\"],\n memory: row.memory,\n disk: row.disk,\n }));\n }\n\n /**\n * Executes EXPLAIN ANALYZE on a Dr