import type { RuntimeDriverDescriptor, RuntimeDriverInstance, } from '@prisma-next/framework-components/execution'; import type { PreparedExecuteRequest, SqlConnection, SqlDriver, SqlExecuteRequest, SqlExplainResult, SqlQueryResult, } from '@prisma-next/sql-relational-core/ast'; import { postgresDriverDescriptorMeta } from '../core/descriptor-meta'; import { createBoundDriverFromBinding, type PostgresBinding, type PostgresDriverCreateOptions, } from '../postgres-driver'; export type PostgresRuntimeDriver = RuntimeDriverInstance<'sql', 'postgres'> & SqlDriver; const USE_BEFORE_CONNECT_MESSAGE = 'Postgres driver not connected. Call connect(binding) before acquireConnection or execute.'; const ALREADY_CONNECTED_MESSAGE = 'Postgres driver already connected. Call close() before reconnecting with a new binding.'; interface DriverRuntimeError extends Error { readonly code: 'DRIVER.NOT_CONNECTED' | 'DRIVER.ALREADY_CONNECTED'; readonly category: 'RUNTIME'; readonly severity: 'error'; readonly details?: Record; } function driverError( code: DriverRuntimeError['code'], message: string, details?: Record, ): DriverRuntimeError { const error = new Error(message) as DriverRuntimeError; Object.defineProperty(error, 'name', { value: 'RuntimeError', configurable: true, }); return Object.assign(error, { code, category: 'RUNTIME' as const, severity: 'error' as const, message, details, }); } function unboundExecute(): AsyncIterable { return { [Symbol.asyncIterator]() { return { async next() { throw driverError('DRIVER.NOT_CONNECTED', USE_BEFORE_CONNECT_MESSAGE); }, }; }, }; } class PostgresUnboundDriverImpl implements PostgresRuntimeDriver { readonly familyId = 'sql' as const; readonly targetId = 'postgres' as const; #delegate: SqlDriver | null = null; #closed = false; #cursorOpts: PostgresDriverCreateOptions['cursor']; #preparedStatements: PostgresDriverCreateOptions['preparedStatements']; constructor(options?: PostgresDriverCreateOptions) { this.#cursorOpts = options?.cursor; this.#preparedStatements = options?.preparedStatements; } get state(): 'unbound' | 'connected' | 'closed' { if (this.#delegate !== null) { return 'connected'; } if (this.#closed) { return 'closed'; } return 'unbound'; } #requireDelegate(): SqlDriver { const delegate = this.#delegate; if (delegate === null) { throw driverError('DRIVER.NOT_CONNECTED', USE_BEFORE_CONNECT_MESSAGE); } return delegate; } async connect(binding: PostgresBinding): Promise { if (this.#delegate !== null) { throw driverError('DRIVER.ALREADY_CONNECTED', ALREADY_CONNECTED_MESSAGE, { bindingKind: binding.kind, }); } this.#delegate = createBoundDriverFromBinding(binding, this.#cursorOpts, { preparedStatements: this.#preparedStatements, }); this.#closed = false; } async acquireConnection(): Promise { const delegate = this.#requireDelegate(); const connection = await delegate.acquireConnection(); return this.#wrapConnection(connection, delegate); } /** * Wraps an acquired connection so that teardown paths which close the * underlying delegate (notably `destroy()` on a pgClient binding, where * the single socket means a destroyed connection invalidates the driver) * also reset our own `#delegate` reference. Without this, a failed * transaction rollback would leave the outer unbound wrapper reporting * `connected` while routing subsequent work to an already-ended delegate. */ #wrapConnection(connection: SqlConnection, delegate: SqlDriver): SqlConnection { const syncDelegateState = (): void => { if (this.#delegate === delegate && delegate.state === 'closed') { this.#delegate = null; this.#closed = true; } }; const wrapped: SqlConnection = { beginTransaction: connection.beginTransaction.bind(connection), execute: connection.execute.bind(connection), executePrepared: connection.executePrepared.bind(connection), query: connection.query.bind(connection), release: async () => { try { await connection.release(); } finally { syncDelegateState(); } }, destroy: async (reason?: unknown) => { try { await connection.destroy(reason); } finally { syncDelegateState(); } }, }; if (connection.explain) { wrapped.explain = connection.explain.bind(connection); } return wrapped; } async close(): Promise { const delegate = this.#delegate; if (delegate !== null) { this.#delegate = null; await delegate.close(); } this.#closed = true; } execute>(request: SqlExecuteRequest): AsyncIterable { const delegate = this.#delegate; if (delegate === null) { return unboundExecute(); } return delegate.execute(request); } executePrepared>( request: PreparedExecuteRequest, ): AsyncIterable { const delegate = this.#delegate; if (delegate === null) { return unboundExecute(); } return delegate.executePrepared(request); } async explain(request: SqlExecuteRequest): Promise { const delegate = this.#requireDelegate(); const explain = delegate.explain; if (explain === undefined) { throw driverError('DRIVER.NOT_CONNECTED', USE_BEFORE_CONNECT_MESSAGE); } return explain.call(delegate, request); } async query>( sql: string, params?: readonly unknown[], ): Promise> { const delegate = this.#requireDelegate(); return delegate.query(sql, params); } } const postgresRuntimeDriverDescriptor: RuntimeDriverDescriptor< 'sql', 'postgres', PostgresDriverCreateOptions, PostgresRuntimeDriver > = { ...postgresDriverDescriptorMeta, create(options?: PostgresDriverCreateOptions): PostgresRuntimeDriver { return new PostgresUnboundDriverImpl(options); }, }; export default postgresRuntimeDriverDescriptor; export type { PostgresBinding, PostgresDriverCreateOptions, QueryResult, } from '../postgres-driver';