import sqliteAdapter from '@prisma-next/adapter-sqlite/runtime'; import type { Contract } from '@prisma-next/contract/types'; import type { SqliteBinding } from '@prisma-next/driver-sqlite/runtime'; import sqliteDriver from '@prisma-next/driver-sqlite/runtime'; import { instantiateExecutionStack } from '@prisma-next/framework-components/execution'; import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir'; import { sql as sqlBuilder } from '@prisma-next/sql-builder/runtime'; import type { Db } from '@prisma-next/sql-builder/types'; import type { ExtractCodecTypes, SqlStorage } from '@prisma-next/sql-contract/types'; import { orm as ormBuilder } from '@prisma-next/sql-orm-client'; import type { CodecTypesBase, RawSqlTag } from '@prisma-next/sql-relational-core/expression'; import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan'; import type { BindSiteParams, Declaration, ExecutionContext, ParamsFromDeclaration, PreparedStatement, Runtime, SqlExecutionStackWithDriver, SqlMiddleware, SqlRuntimeExtensionDescriptor, TransactionContext, VerifyMarkerOption, } from '@prisma-next/sql-runtime'; import { createExecutionContext, createSqlExecutionStack, withTransaction, } from '@prisma-next/sql-runtime'; import sqliteTarget, { SqliteContractSerializer as SqlContractSerializer, } from '@prisma-next/target-sqlite/runtime'; import { assertDefined } from '@prisma-next/utils/assertions'; import { blindCast, castAs } from '@prisma-next/utils/casts'; import { ifDefined } from '@prisma-next/utils/defined'; import { buildSqliteStaticContext, type SqliteStaticContext } from '../static/sqlite-static'; import { resolveOptionalSqliteBinding, resolveSqliteBinding } from './binding'; import { SqliteRuntimeImpl } from './sqlite-runtime'; export type SqliteTargetId = 'sqlite'; type OrmClient> = ReturnType>; type UnboundSql> = Db[typeof UNBOUND_NAMESPACE_ID]; type UnboundOrm> = OrmClient[typeof UNBOUND_NAMESPACE_ID]; function unboundOrm>( orm: OrmClient, ): UnboundOrm { const value = orm[UNBOUND_NAMESPACE_ID]; assertDefined(value, 'the unbound namespace always exists on a sqlite builder output'); return blindCast< UnboundOrm, 'OrmClient indexed by a literal key widens NsId to string; Collection is invariant in NsId via row/mutation-input types, so the indexed-access type cannot be proven to match the literal-keyed OrmNamespace without this cast' >(value); } export interface SqliteTransactionContext> extends TransactionContext { readonly sql: UnboundSql; readonly orm: UnboundOrm; readonly enums: SqliteStaticContext['enums']; } export interface SqliteClient> { readonly sql: UnboundSql; readonly orm: UnboundOrm; readonly enums: SqliteStaticContext['enums']; readonly raw: RawSqlTag; readonly context: ExecutionContext; readonly contract: TContract; readonly stack: SqlExecutionStackWithDriver; connect(bindingInput?: { readonly path: string }): Promise; runtime(): Runtime; prepare< D extends Declaration, Row, CT extends CodecTypesBase = ExtractCodecTypes & CodecTypesBase, >( declaration: D, callback: (sql: UnboundSql, params: BindSiteParams) => SqlQueryPlan, ): Promise, Row>>; transaction(fn: (tx: SqliteTransactionContext) => PromiseLike): Promise; close(): Promise; [Symbol.asyncDispose](): Promise; } export interface SqliteOptionsBase { readonly extensions?: readonly SqlRuntimeExtensionDescriptor[]; readonly middleware?: readonly SqlMiddleware[]; readonly verifyMarker?: VerifyMarkerOption; } export type SqliteOptionsWithContract> = { readonly path?: string; } & SqliteOptionsBase & { readonly contract: TContract; readonly contractJson?: never; }; export type SqliteOptionsWithContractJson> = { readonly path?: string; readonly _contract?: TContract; } & SqliteOptionsBase & { readonly contractJson: unknown; readonly contract?: never; }; export type SqliteOptions> = | SqliteOptionsWithContract | SqliteOptionsWithContractJson; function resolveContract>( options: SqliteOptions, ): TContract { const serializer = new SqlContractSerializer(); if ('contractJson' in options && options.contractJson !== undefined) { return serializer.deserializeContract(options.contractJson) as TContract; } const contract = (options as SqliteOptionsWithContract).contract; return serializer.deserializeContract(serializer.serializeContract(contract)) as TContract; } export default function sqlite>( options: SqliteOptionsWithContract, ): SqliteClient; export default function sqlite>( options: SqliteOptionsWithContractJson, ): SqliteClient; export default function sqlite>( options: SqliteOptions, ): SqliteClient { const contract = resolveContract(options); let binding = resolveOptionalSqliteBinding(options); const stack = createSqlExecutionStack({ target: sqliteTarget, adapter: sqliteAdapter, driver: sqliteDriver, extensionPacks: options.extensions ?? [], }); const context = createExecutionContext({ contract, stack, driver: sqliteDriver, }); const { sql, raw: rawSqlTag, enums, }: SqliteStaticContext = buildSqliteStaticContext( context, stack.adapter.rawCodecInferer, ); let runtimeInstance: Runtime | undefined; let runtimeDriver: { connect(binding: unknown): Promise } | undefined; let driverConnected = false; let connectPromise: Promise | undefined; let closePromise: Promise | undefined; let backgroundConnectError: unknown; let closed = false; let ownedDispose: (() => Promise) | undefined; const connectDriver = async (resolvedBinding: SqliteBinding): Promise => { if (driverConnected) return; if (!runtimeDriver) throw new Error('SQLite runtime driver missing'); if (connectPromise) return connectPromise; connectPromise = runtimeDriver .connect(resolvedBinding) .then(() => { driverConnected = true; }) .catch((err) => { backgroundConnectError = err; connectPromise = undefined; throw err; }); return connectPromise; }; const getRuntime = (): Runtime => { if (closed) { throw new Error('SQLite client is closed'); } if (backgroundConnectError !== undefined) { throw backgroundConnectError; } if (runtimeInstance) { return runtimeInstance; } const stackInstance = instantiateExecutionStack(stack); const driverDescriptor = stack.driver; if (!driverDescriptor) { throw new Error('Driver descriptor missing from execution stack'); } const driver = driverDescriptor.create(); ownedDispose = () => driver.close(); runtimeDriver = driver; if (binding !== undefined) { void connectDriver(binding).catch(() => undefined); } runtimeInstance = new SqliteRuntimeImpl({ context, adapter: stackInstance.adapter, driver, ...ifDefined('verifyMarker', options.verifyMarker), ...ifDefined('middleware', options.middleware), }); return runtimeInstance; }; const orm: UnboundOrm = unboundOrm( ormBuilder({ context, runtime: { execute(plan) { return getRuntime().execute(plan); }, connection() { return getRuntime().connection(); }, }, }), ); return { sql, orm, enums, raw: rawSqlTag, context, contract, stack, async connect(bindingInput) { if (closed) { throw new Error('SQLite client is closed'); } if (driverConnected || connectPromise) { throw new Error('SQLite client already connected'); } backgroundConnectError = undefined; if (bindingInput !== undefined) { binding = resolveSqliteBinding(bindingInput); } if (binding === undefined) { throw new Error( 'SQLite binding not configured. Pass path to sqlite(...) or call db.connect({ path }).', ); } const runtime = getRuntime(); if (driverConnected) { return runtime; } await connectDriver(binding); return runtime; }, runtime() { return getRuntime(); }, prepare< D extends Declaration, Row, CT extends CodecTypesBase = ExtractCodecTypes & CodecTypesBase, >( declaration: D, callback: (sql: UnboundSql, params: BindSiteParams) => SqlQueryPlan, ): Promise, Row>> { return getRuntime().prepare(declaration, (params) => callback(sql, params)); }, transaction(fn: (tx: SqliteTransactionContext) => PromiseLike): Promise { let runtime: ReturnType; try { runtime = getRuntime(); } catch (err) { return Promise.reject(err); } return withTransaction(runtime, (txCtx) => { const rawCodecInferer = stack.adapter.rawCodecInferer; const txSqlNamespace = sqlBuilder({ context, rawCodecInferer })[ UNBOUND_NAMESPACE_ID ]; assertDefined( txSqlNamespace, 'the unbound namespace always exists on a sqlite builder output', ); const txSql: UnboundSql = blindCast< UnboundSql, 'Db indexed by a literal key widens NsId to string; TableProxy is invariant in NsId via insert()/update() parameter positions, so the indexed-access type cannot be proven to match the literal-keyed Namespace without this cast' >(txSqlNamespace); const txOrm: UnboundOrm = unboundOrm( ormBuilder({ runtime: { execute(plan) { return txCtx.execute(plan); }, }, context, }), ); // Use `txCtx` as the prototype instead of spreading it so that live // accessors (notably the `invalidated` getter, which reads a closure // variable in `withTransaction`) remain wired to the original object. // Spreading would evaluate the getter once and freeze its value. const tx: SqliteTransactionContext = Object.assign( castAs(Object.create(txCtx)), { sql: txSql, orm: txOrm, enums }, ); return fn(tx); }); }, close(): Promise { if (closePromise) return closePromise; closed = true; closePromise = (async () => { await connectPromise?.catch(() => undefined); await ownedDispose?.(); })(); return closePromise; }, [Symbol.asyncDispose](): Promise { return this.close(); }, }; }