UNPKG

@ydbjs/query

Version:

High-level, type-safe YQL query and transaction client for YDB. Supports tagged template syntax, parameter binding, transactions, and statistics.

186 lines 9.27 kB
import { StatusIds_StatusCode } from '@ydbjs/api/operation'; import { QueryServiceDefinition } from '@ydbjs/api/query'; import { CommitError, YDBError } from '@ydbjs/error'; import { defaultRetryConfig, isRetryableError, retry } from '@ydbjs/retry'; import { loggers } from '@ydbjs/debug'; import { Query } from './query.js'; import { ctx } from './ctx.js'; import { UnsafeString, identifier, unsafe, yql } from './yql.js'; let dbg = loggers.query; const doImpl = function () { throw new Error('Not implemented'); }; /** * Creates a query client for executing YQL queries and managing transactions. * * @param driver - The YDB driver instance used to communicate with the database. * @returns A `QueryClient` object that provides methods for executing queries and managing transactions. * * @remarks * The returned client provides a tagged template function for YQL queries, as well as transactional helpers. * * @example * ```typescript * const client = query(driver); * const result = await client`SELECT 1;`; * ``` * * @example * ```typescript * await client.transaction(async (yql, signal) => { * const res = await yql`SELECT * FROM users WHERE id = ${userId}`; * // ... * }); * ``` * * @see {@link QueryClient} */ export function query(driver) { function yqlQuery(strings, ...values) { let { text, params } = yql(strings, ...values); dbg.log('creating query instance for text: %s', text); return ctx.run(ctx.getStore() ?? {}, () => new Query(driver, text, params)); } /** * Executes a transactional operation with automatic session and transaction management, * including retries on retryable errors. * * This function handles the lifecycle of a YDB session and transaction, including session creation, * transaction begin, commit, and rollback. It also manages retries for retryable errors and ensures * proper cleanup of resources. The transaction isolation level and idempotency can be configured. * * @template T The return type of the transactional operation. * @param optOrFn - Either the transaction execution options or the transactional callback function. * If a function is provided as the first argument, it is used as the transactional callback and * default options are applied. * @param fn - The transactional callback function, if options are provided as the first argument. * The callback receives a YQL query executor and an AbortSignal. * @returns A promise that resolves with the result of the transactional operation. * @throws {YDBError} If session creation, transaction begin, or commit fails. * @throws {CommitError} If the transaction commit fails. * @throws {Error} If a non-retryable error occurs during the transaction. * * @remarks * - The function automatically retries the transaction on retryable errors if the operation is idempotent. * - The session and transaction are automatically cleaned up after execution. * - The transaction isolation level defaults to "serializableReadWrite" if not specified. * - The function uses the driver's QueryServiceDefinition to interact with YDB. */ async function txIml(optOrFn, fn) { dbg.log('starting transaction'); await driver.ready(); let store = ctx.getStore() || {}; let client = driver.createClient(QueryServiceDefinition); let caller = (typeof optOrFn === "function" ? optOrFn : fn); let options = typeof optOrFn === "function" ? {} : optOrFn; options.isolation ??= "serializableReadWrite"; options.idempotent = options.idempotent ?? false; return retry({ ...defaultRetryConfig, signal: options.signal, idempotent: true, onRetry: (ctx) => { dbg.log('retrying transaction, attempt %d, error: %O', ctx.attempt, ctx.error); } }, async (signal) => { dbg.log('creating session for transaction'); let sessionResponse = await client.createSession({}, { signal }); if (sessionResponse.status !== StatusIds_StatusCode.SUCCESS) { dbg.log('failed to create session, status: %d', sessionResponse.status); throw new YDBError(sessionResponse.status, sessionResponse.issues); } store.signal = signal; store.nodeId = sessionResponse.nodeId; store.sessionId = sessionResponse.sessionId; client = driver.createClient(QueryServiceDefinition, sessionResponse.nodeId); let attachSession = client.attachSession({ sessionId: store.sessionId }, { signal })[Symbol.asyncIterator](); let attachSessionResult = await attachSession.next(); if (attachSessionResult.value.status !== StatusIds_StatusCode.SUCCESS) { dbg.log('failed to attach session, status: %d', attachSessionResult.value.status); throw new YDBError(attachSessionResult.value.status, attachSessionResult.value.issues); } dbg.log('session %s created and attached', store.sessionId); let beginTransactionResult = await client.beginTransaction({ sessionId: store.sessionId, txSettings: { txMode: { case: options.isolation, value: {} } }, }, { signal }); if (beginTransactionResult.status !== StatusIds_StatusCode.SUCCESS) { dbg.log('failed to begin transaction, status: %d', beginTransactionResult.status); throw new YDBError(beginTransactionResult.status, beginTransactionResult.issues); } store.transactionId = beginTransactionResult.txMeta.id; let сommitHooks = []; let rollbackHooks = []; let closeHooks = []; let commited = false; try { let tx = Object.assign(yqlQuery, { nodeId: store.nodeId, sessionId: store.sessionId, transactionId: store.transactionId, onRollback: (fn) => { rollbackHooks.push(fn); }, onCommit: (fn) => { сommitHooks.push(fn); }, onClose: (fn) => { closeHooks.push(fn); }, }); dbg.log('executing transaction body'); let result = await ctx.run(store, () => caller(tx, signal)); dbg.log('executing %d commit hooks', сommitHooks.length); await Promise.all(сommitHooks.map(async (hook, i) => { dbg.log('executing commit hook #%d', i + 1); await hook(signal); dbg.log('commit hook #%d completed', i + 1); })); dbg.log('committing transaction'); let commitResult = await client.commitTransaction({ sessionId: store.sessionId, txId: store.transactionId }, { signal }); if (commitResult.status !== StatusIds_StatusCode.SUCCESS) { dbg.log('failed to commit transaction, status: %d', commitResult.status); throw new CommitError("Transaction commit failed.", new YDBError(commitResult.status, commitResult.issues)); } commited = true; dbg.log('transaction committed successfully'); return result; } catch (error) { dbg.log('transaction error: %O', error); dbg.log('executing %d rollback hooks', сommitHooks.length); await Promise.all(rollbackHooks.map(async (hook, i) => { dbg.log('executing rollback hook #%d', i + 1); await hook(error, signal); dbg.log('rollback hook #%d completed', i + 1); })); void client.rollbackTransaction({ sessionId: store.sessionId, txId: store.transactionId }); if (!isRetryableError(error, options.idempotent)) { dbg.log('transaction not retryable, aborting'); throw new Error("Transaction failed.", { cause: error }); } throw error; } finally { dbg.log('deleting session %s', sessionResponse.sessionId); void client.deleteSession({ sessionId: sessionResponse.sessionId }); dbg.log('executing %d close hooks', сommitHooks.length); await Promise.all(closeHooks.map(async (hook, i) => { dbg.log('executing close hook #%d', i + 1); await hook(commited, signal); dbg.log('close hook #%d completed', i + 1); })); } }); } return Object.assign(yqlQuery, { do: doImpl, begin: txIml, transaction: txIml, identifier: identifier, unsafe: unsafe, async [Symbol.asyncDispose]() { }, }); } export { identifier, unsafe, UnsafeString } from './yql.js'; //# sourceMappingURL=index.js.map