import { type Cache, hashQuery, NoopCache } from '~/cache/core/cache.ts'; import type { WithCacheConfig } from '~/cache/core/types.ts'; import { entityKind, is } from '~/entity.ts'; import { DrizzleQueryError, TransactionRollbackError } from '~/errors.ts'; import type { TablesRelationalConfig } from '~/relations.ts'; import type { PreparedQuery } from '~/session.ts'; import type { Query, SQL } from '~/sql/index.ts'; import { tracer } from '~/tracing.ts'; import type { NeonAuthToken } from '~/utils.ts'; import { GelDatabase } from './db.ts'; import type { GelDialect } from './dialect.ts'; import type { SelectedFieldsOrdered } from './query-builders/select.types.ts'; export interface PreparedQueryConfig { execute: unknown; all: unknown; values: unknown; } export abstract class GelPreparedQuery implements PreparedQuery { constructor( protected query: Query, private cache?: Cache, // per query related metadata private queryMetadata?: { type: 'select' | 'update' | 'delete' | 'insert'; tables: string[]; } | undefined, // config that was passed through $withCache private cacheConfig?: WithCacheConfig, ) { // it means that no $withCache options were passed and it should be just enabled if (cache && cache.strategy() === 'all' && cacheConfig === undefined) { this.cacheConfig = { enable: true, autoInvalidate: true }; } if (!this.cacheConfig?.enable) { this.cacheConfig = undefined; } } /** @internal */ protected async queryWithCache( queryString: string, params: any[], query: () => Promise, ): Promise { if (this.cache === undefined || is(this.cache, NoopCache) || this.queryMetadata === undefined) { try { return await query(); } catch (e) { throw new DrizzleQueryError(queryString, params, e as Error); } } // don't do any mutations, if globally is false if (this.cacheConfig && !this.cacheConfig.enable) { try { return await query(); } catch (e) { throw new DrizzleQueryError(queryString, params, e as Error); } } // For mutate queries, we should query the database, wait for a response, and then perform invalidation if ( ( this.queryMetadata.type === 'insert' || this.queryMetadata.type === 'update' || this.queryMetadata.type === 'delete' ) && this.queryMetadata.tables.length > 0 ) { try { const [res] = await Promise.all([ query(), this.cache.onMutate({ tables: this.queryMetadata.tables }), ]); return res; } catch (e) { throw new DrizzleQueryError(queryString, params, e as Error); } } // don't do any reads if globally disabled if (!this.cacheConfig) { try { return await query(); } catch (e) { throw new DrizzleQueryError(queryString, params, e as Error); } } if (this.queryMetadata.type === 'select') { const fromCache = await this.cache.get( this.cacheConfig.tag ?? await hashQuery(queryString, params), this.queryMetadata.tables, this.cacheConfig.tag !== undefined, this.cacheConfig.autoInvalidate, ); if (fromCache === undefined) { let result; try { result = await query(); } catch (e) { throw new DrizzleQueryError(queryString, params, e as Error); } // put actual key await this.cache.put( this.cacheConfig.tag ?? await hashQuery(queryString, params), result, // make sure we send tables that were used in a query only if user wants to invalidate it on each write this.cacheConfig.autoInvalidate ? this.queryMetadata.tables : [], this.cacheConfig.tag !== undefined, this.cacheConfig.config, ); // put flag if we should invalidate or not return result; } return fromCache as unknown as T; } try { return await query(); } catch (e) { throw new DrizzleQueryError(queryString, params, e as Error); } } protected authToken?: NeonAuthToken; getQuery(): Query { return this.query; } mapResult(response: unknown, _isFromBatch?: boolean): unknown { return response; } static readonly [entityKind]: string = 'GelPreparedQuery'; /** @internal */ joinsNotNullableMap?: Record; abstract execute(placeholderValues?: Record): Promise; /** @internal */ abstract all(placeholderValues?: Record): Promise; /** @internal */ abstract isResponseInArrayMode(): boolean; } export abstract class GelSession< TQueryResult extends GelQueryResultHKT = any, // TO TFullSchema extends Record = Record, TSchema extends TablesRelationalConfig = Record, > { static readonly [entityKind]: string = 'GelSession'; constructor(protected dialect: GelDialect) {} abstract prepareQuery( query: Query, fields: SelectedFieldsOrdered | undefined, name: string | undefined, isResponseInArrayMode: boolean, customResultMapper?: (rows: unknown[][], mapColumnValue?: (value: unknown) => unknown) => T['execute'], queryMetadata?: { type: 'select' | 'update' | 'delete' | 'insert'; tables: string[]; }, cacheConfig?: WithCacheConfig, ): GelPreparedQuery; execute(query: SQL): Promise { return tracer.startActiveSpan('drizzle.operation', () => { const prepared = tracer.startActiveSpan('drizzle.prepareQuery', () => { return this.prepareQuery( this.dialect.sqlToQuery(query), undefined, undefined, false, ); }); return prepared.execute(undefined); }); } all(query: SQL): Promise { return this.prepareQuery( this.dialect.sqlToQuery(query), undefined, undefined, false, ).all(); } async count(sql: SQL): Promise { const res = await this.execute<[{ count: string }]>(sql); return Number( res[0]['count'], ); } abstract transaction( transaction: (tx: GelTransaction) => Promise, ): Promise; } export abstract class GelTransaction< TQueryResult extends GelQueryResultHKT, TFullSchema extends Record = Record, TSchema extends TablesRelationalConfig = Record, > extends GelDatabase { static override readonly [entityKind]: string = 'GelTransaction'; constructor( dialect: GelDialect, session: GelSession, protected schema: { fullSchema: Record; schema: TSchema; tableNamesMap: Record; } | undefined, ) { super(dialect, session, schema); } rollback(): never { throw new TransactionRollbackError(); } abstract override transaction( transaction: (tx: GelTransaction) => Promise, ): Promise; } export interface GelQueryResultHKT { readonly $brand: 'GelQueryResultHKT'; readonly row: unknown; readonly type: unknown; } export type GelQueryResultKind = (TKind & { readonly row: TRow; })['type'];