Files
drizzle-team--drizzle-orm/drizzle-orm/src/gel-core/session.ts
T
2026-07-13 12:10:05 +08:00

248 lines
7.0 KiB
TypeScript

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<T extends PreparedQueryConfig> 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<T>(
queryString: string,
params: any[],
query: () => Promise<T>,
): Promise<T> {
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<string, boolean>;
abstract execute(placeholderValues?: Record<string, unknown>): Promise<T['execute']>;
/** @internal */
abstract all(placeholderValues?: Record<string, unknown>): Promise<T['all']>;
/** @internal */
abstract isResponseInArrayMode(): boolean;
}
export abstract class GelSession<
TQueryResult extends GelQueryResultHKT = any, // TO
TFullSchema extends Record<string, unknown> = Record<string, never>,
TSchema extends TablesRelationalConfig = Record<string, never>,
> {
static readonly [entityKind]: string = 'GelSession';
constructor(protected dialect: GelDialect) {}
abstract prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(
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<T>;
execute<T>(query: SQL): Promise<T> {
return tracer.startActiveSpan('drizzle.operation', () => {
const prepared = tracer.startActiveSpan('drizzle.prepareQuery', () => {
return this.prepareQuery<PreparedQueryConfig & { execute: T }>(
this.dialect.sqlToQuery(query),
undefined,
undefined,
false,
);
});
return prepared.execute(undefined);
});
}
all<T = unknown>(query: SQL): Promise<T[]> {
return this.prepareQuery<PreparedQueryConfig & { all: T[] }>(
this.dialect.sqlToQuery(query),
undefined,
undefined,
false,
).all();
}
async count(sql: SQL): Promise<number> {
const res = await this.execute<[{ count: string }]>(sql);
return Number(
res[0]['count'],
);
}
abstract transaction<T>(
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
): Promise<T>;
}
export abstract class GelTransaction<
TQueryResult extends GelQueryResultHKT,
TFullSchema extends Record<string, unknown> = Record<string, never>,
TSchema extends TablesRelationalConfig = Record<string, never>,
> extends GelDatabase<TQueryResult, TFullSchema, TSchema> {
static override readonly [entityKind]: string = 'GelTransaction';
constructor(
dialect: GelDialect,
session: GelSession<any, any, any>,
protected schema: {
fullSchema: Record<string, unknown>;
schema: TSchema;
tableNamesMap: Record<string, string>;
} | undefined,
) {
super(dialect, session, schema);
}
rollback(): never {
throw new TransactionRollbackError();
}
abstract override transaction<T>(
transaction: (tx: GelTransaction<TQueryResult, TFullSchema, TSchema>) => Promise<T>,
): Promise<T>;
}
export interface GelQueryResultHKT {
readonly $brand: 'GelQueryResultHKT';
readonly row: unknown;
readonly type: unknown;
}
export type GelQueryResultKind<TKind extends GelQueryResultHKT, TRow> = (TKind & {
readonly row: TRow;
})['type'];