All files / third-parties/drizzle-adapter/src/ydb driver.ts

95.83% Statements 46/48
92.85% Branches 26/28
100% Functions 17/17
95.55% Lines 43/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228                                                                                                                378x 272x     106x       1x           105x 105x                   388x   388x 350x     388x 388x 378x 378x                           17x 1x             14x     16x 1x     15x 2x     13x       17x               24x                                           16x 5x 5x 11x 11x 11x           16x           381x 381x       3x       12x                 364x             17x   17x 16x     1x                 12x 11x         2x   2x          
import { Driver, kRegisterLibrary } from '@ydbjs/core'
import { type QueryClient, type TX, query as createQueryClient } from '@ydbjs/query'
import { fromJs } from '@ydbjs/value'
 
import pkg from '../../package.json' with { type: 'json' }
export interface YdbTransactionConfig {
	accessMode?: 'read only' | 'read write' | undefined
	isolationLevel?: 'serializableReadWrite' | 'snapshotReadOnly' | undefined
	idempotent?: boolean | undefined
}
 
export type YdbExecutionMethod = 'all' | 'execute'
 
export interface YdbExecuteOptions {
	arrayMode?: boolean | undefined
	typings?: unknown[] | undefined
}
 
export interface YdbQueryMeta {
	arrayMode: boolean
	typings?: unknown[] | undefined
}
 
export interface YdbQueryResult {
	rows: unknown[]
	rowCount?: number
	command?: YdbExecutionMethod
	meta?: YdbQueryMeta
}
 
export type YdbRemoteCallback = (
	sql: string,
	params: unknown[],
	method: YdbExecutionMethod,
	options?: YdbExecuteOptions
) => Promise<YdbQueryResult>
 
export interface YdbExecutor {
	execute(
		sql: string,
		params: unknown[],
		method: YdbExecutionMethod,
		options?: YdbExecuteOptions
	): Promise<YdbQueryResult>
	ready?(signal?: AbortSignal): Promise<void>
	close?(): Promise<void> | void
}
 
export interface YdbTransactionalExecutor extends YdbExecutor {
	transaction<T>(
		callback: (tx: YdbExecutor) => Promise<T>,
		config?: YdbTransactionConfig
	): Promise<T>
}
 
function getRows<T = unknown>(result: unknown): T[] {
	if (!Array.isArray(result) || result.length === 0) {
		return []
	}
 
	if (result.length > 1) {
		// Drizzle generates one statement per call, so multi-result-set responses
		// only happen when the caller (or a raw SQL fragment) packs several
		// statements into one query. Silently dropping the tail hides real bugs.
		throw new Error(
			`YDB query returned ${result.length} result sets, but the Drizzle adapter expects exactly one. ` +
				'Split the query so each call runs a single statement, or use the @ydbjs/query API directly for multi-result-set responses.'
		)
	}
 
	let [rows] = result
	return Array.isArray(rows) ? (rows as T[]) : []
}
 
async function execQuery(
	ql: QueryClient | TX,
	text: string,
	params: unknown[],
	method: YdbExecutionMethod,
	options?: YdbExecuteOptions
): Promise<YdbQueryResult> {
	let query = ql(text)
 
	for (let i = 0; i < params.length; i++) {
		query = query.parameter(`p${i}`, fromJs(params[i] as any))
	}
 
	let executedQuery = options?.arrayMode ? query.values() : query
	let raw = await executedQuery
	let rows = getRows(raw)
	return {
		rows,
		rowCount: rows.length,
		command: method,
		meta: {
			arrayMode: options?.arrayMode === true,
			typings: options?.typings ? [...options.typings] : undefined,
		},
	}
}
 
function mapTransactionConfig(
	config?: YdbTransactionConfig
): { isolation?: 'serializableReadWrite' | 'snapshotReadOnly'; idempotent?: boolean } | undefined {
	if (!config) {
		return undefined
	}
 
	function withIdempotent(
		isolation: 'serializableReadWrite' | 'snapshotReadOnly',
		idempotent?: boolean
	): { isolation: 'serializableReadWrite' | 'snapshotReadOnly'; idempotent?: boolean } {
		return idempotent === undefined ? { isolation } : { isolation, idempotent }
	}
 
	if (config.isolationLevel) {
		return withIdempotent(config.isolationLevel, config.idempotent)
	}
 
	if (config.accessMode === 'read only') {
		return { isolation: 'snapshotReadOnly', idempotent: true }
	}
 
	return withIdempotent('serializableReadWrite', config.idempotent)
}
 
class YdbTxExecutor implements YdbExecutor {
	constructor(private readonly tx: TX) {}
 
	execute(
		sql: string,
		params: unknown[],
		method: YdbExecutionMethod,
		options?: YdbExecuteOptions
	): Promise<YdbQueryResult> {
		return execQuery(this.tx, sql, params, method, options)
	}
}
 
export interface YdbDriverOptions {
	connectionString: string
}
 
export class YdbDriver implements YdbTransactionalExecutor {
	readonly driver: Driver
	#ownsDriver: boolean
	#client: QueryClient | undefined
 
	constructor(connectionString: string)
	constructor(options: YdbDriverOptions)
	/**
	 * Wraps an existing YDB driver instance.
	 *
	 * @param driver Existing YDB driver instance. The adapter does not close borrowed drivers.
	 */
	constructor(driver: Driver)
	constructor(arg: string | YdbDriverOptions | Driver) {
		if (arg instanceof Driver) {
			this.driver = arg
			this.#ownsDriver = false
		} else if (typeof arg === 'string') {
			this.driver = new Driver(arg)
			this.#ownsDriver = true
		} else E{
			this.driver = new Driver(arg.connectionString)
			this.#ownsDriver = true
		}
 
		this.driver[kRegisterLibrary]('@ydbjs/drizzle-adapter', pkg.version)
	}
 
	// Lazy: avoid constructing SessionPool until the first query/transaction.
	// Setter exists so callers (and tests) can swap in a pre-built QueryClient.
	get client(): QueryClient {
		this.#client ??= createQueryClient(this.driver)
		return this.#client
	}
 
	set client(value: QueryClient) {
		this.#client = value
	}
 
	async ready(signal?: AbortSignal): Promise<void> {
		await this.driver.ready(signal)
	}
 
	execute(
		sql: string,
		params: unknown[],
		method: YdbExecutionMethod,
		options?: YdbExecuteOptions
	): Promise<YdbQueryResult> {
		return execQuery(this.client, sql, params, method, options)
	}
 
	async transaction<T>(
		callback: (tx: YdbExecutor) => Promise<T>,
		config?: YdbTransactionConfig
	): Promise<T> {
		let options = mapTransactionConfig(config)
 
		if (options) {
			return this.client.begin(options, async (tx) => callback(new YdbTxExecutor(tx)))
		}
 
		return this.client.begin(async (tx) => callback(new YdbTxExecutor(tx)))
	}
 
	/**
	 * Closes the owned YDB driver instance.
	 *
	 * Borrowed driver instances passed to the constructor are not closed.
	 */
	close(): void {
		if (this.#ownsDriver) {
			this.driver.close()
		}
	}
 
	static fromCallback(callback: YdbRemoteCallback): YdbExecutor {
		return {
			execute(sql, params, method, options) {
				return callback(sql, params, method, options)
			},
		}
	}
}