드라이버 작성자용 바인딩 가이드
이 가이드는 사용자 정의 QueryExecutor, ConnectionProvider, StatementBindingAdapter를 구현하는 방법을 설명합니다. 기본 제공 어댑터의 지원 등급은 런타임·드라이버 지원 매트릭스를 참고하세요.
물리 계층 SPI는 동기 결과를 받을 수 있습니다. 애플리케이션 API는 계속 비동기입니다.
type Awaitable<T> = T | PromiseLike<T>;QueryExecutor.query, call, 선택 사항인 bulk, 트랜잭션 제어 메서드는 Awaitable을 반환할 수 있습니다. ConnectionProvider.acquire()는 계속 Promise를 반환하고, stream()은 계속 AsyncIterable입니다. 따라서 동기 네이티브 이터레이터는 얇은 async generator 어댑터로 감싸야 합니다. 이렇게 해야 정리 동작과 범위 동작이 유지됩니다.
논리 문장의 불변 조건
섹션 제목: “논리 문장의 불변 조건”core/template 렌더링은 변경할 수 없는 RenderedStatement 하나를 반환합니다.
interface RenderedStatement { readonly segments: readonly string[]; readonly parameters: readonly RenderedParameter[]; readonly nativeTemplate?: TemplateStringsArray; readonly dialectId: string; readonly resultKind: "rows" | "command" | "call" | "unknown"; readonly routineProcedure?: RoutineProcedure; readonly fingerprint?: string; readonly variantFingerprint?: string;}segments.length === parameters.length + 1입니다. 각 파라미터는 값입니다. 직접 만드는 드라이버는 파라미터를 SQL, 식별자, 중첩 SQL, 네이티브 태그 템플릿 명령으로 해석하면 안 됩니다. SQL 구조는 사용자가 명시적인 헬퍼(sql.ident, sql.fragment, sql.raw, sql.list, sql.join)로 작성하며, 이미 segments에 들어 있습니다.
바인딩과 리스
섹션 제목: “바인딩과 리스”interface StatementBindingAdapter { readonly id: string; describe(statement: RenderedStatement, context: StatementBindingContext): StatementBindingDescription; describeBulk?(bulk: RenderedBulk, context: StatementBindingContext): BulkBindingDescription;}interface StatementBindingContext { readonly dialectId: string; readonly requestedReuse: "auto" | "simple" | "reuse"; readonly preparedName?: string; readonly transactionScoped?: boolean;}interface ConnectionProvider { readonly statementBinding: StatementBindingAdapter; readonly environment?: DriverEnvironment; validateTransactionOptions?(options: TransactionOptions): void; acquire(): Promise<ConnectionLease>;}interface ConnectionLease extends QueryExecutor { release(options?: { readonly discard?: boolean }): void | Promise<void>;}describe()와 describeBulk()는 커넥션을 획득하기 전에 실행되는 순수한 변환 단계입니다.
- 힌트, 형태, 전송 방식은 여기서 검증합니다.
- 이 단계에서 실패하면 두 실행 플래그가 모두 false입니다.
- 프로바이더와 리스는 정확히 같은 불변 바인딩 어댑터 객체를 노출해야 합니다.
- 드라이버 전용 요청 객체는 외부에 드러내지 마세요. 예를 들어
StatementBindingDescription을 키로 하는WeakMap에 보관합니다.
준비된 쿼리의 논리적 형태는 결과 종류, 방언, 정규화된 세그먼트, 그리고 순서가 있는 힌트·방향·출력 메타데이터로 정해집니다. $1, ?, :1, @p1은 전송 세부 사항일 뿐 형태 식별에 포함되지 않습니다. 값은 바뀔 수 있지만 구조적 형태는 바뀔 수 없습니다.
실행기 메서드와 취소
섹션 제목: “실행기 메서드와 취소”interface QueryExecutor { readonly ownershipKey?: object; readonly statementBinding: StatementBindingAdapter; readonly environment?: DriverEnvironment; query<Row>( statement: RenderedStatement, binding?: StatementBindingDescription, options?: ExecutionOptions, ): Awaitable<QueryExecutionResult<Row>>; stream<Row>( statement: RenderedStatement, binding?: StatementBindingDescription, options?: ExecutionOptions, ): AsyncIterable<Row>; call( statement: RenderedStatement, binding?: StatementBindingDescription, options?: ExecutionOptions, ): Awaitable<DriverRoutineResult>; bulk?( bulk: RenderedBulk, binding: BulkBindingDescription, options?: ExecutionOptions, ): Awaitable<BulkExecutionResult>; validateTransactionOptions?(options: TransactionOptions): void; begin?(options?: TransactionOptions): Awaitable<void>; commit?(): Awaitable<void>; rollback?(): Awaitable<void>; savepoint?(name: string): Awaitable<void>; rollbackTo?(name: string): Awaitable<void>; releaseSavepoint?(name: string): Awaitable<void>;}- 시그널이 이미 중단된 상태라면 그
reason으로 reject하세요. - 활성 시그널을 받으려면 물리적인 취소 경로가 있어야 합니다. 경로가 없으면 I/O 전에
UnsupportedFeatureError(기능statement.cancel, 코드BRAID_CANCEL_UNSUPPORTED)로 reject하세요. 반복만 멈추는 것은 취소가 아닙니다. stream은 실제 드라이버 경로여야 합니다.call은 리스를 반환하기 전에 모든 커서, 결과 집합, 요청, 출력 전달 객체를 메모리로 읽고 닫아야 합니다.- 스트리밍을 흉내 내려고 버퍼링하지 마세요. 루틴 출력 전달 방식을 추측하지 마세요.
텍스트·위치 기반 바인딩 전체 예제
섹션 제목: “텍스트·위치 기반 바인딩 전체 예제”import { UnsupportedFeatureError, createRenderedStatement, createStatementBindingDescription, type ConnectionLease, type ConnectionProvider, type ExecutionOptions, type QueryExecutionResult, type QueryExecutor, type RenderedStatement, type StatementBindingAdapter, type StatementBindingContext, type StatementBindingDescription,} from "@sqlbraid/core";
interface WireClient { execute<Row>(text: string, values: readonly unknown[]): Promise<QueryExecutionResult<Row>>; release(options?: { discard?: boolean }): void | Promise<void>;}
const requests = new WeakMap< StatementBindingDescription, { statement: RenderedStatement; text: string; values: readonly unknown[] }>();
function assertSignal(options?: ExecutionOptions): void { if (options?.signal?.aborted) throw options.signal.reason; if (options?.signal) { throw new UnsupportedFeatureError( "statement.cancel", "BRAID_CANCEL_UNSUPPORTED", "acme-wire cannot cancel an active statement", ); }}
export const acmeBinding: StatementBindingAdapter = Object.freeze({ id: "acme-wire", describe(statement: RenderedStatement, context: StatementBindingContext) { statement = createRenderedStatement(statement); if (statement.parameters.some((parameter) => parameter.hint !== undefined)) { throw new UnsupportedFeatureError("parameter.hint", "BRAID_BIND_HINT_UNSUPPORTED", "acme-wire has no hint API"); } const binding = createStatementBindingDescription(statement, context, { adapterId: "acme-wire", transport: "text-positional", placeholder: (index) => `$${index}`, reuse: { effective: "simple", owner: "driver" }, }); const text = binding.parameterizedSql; if (text === undefined) throw new TypeError("BRAID_BIND_TRANSPORT: missing parameterized SQL"); requests.set(binding, { statement, text, values: statement.parameters.map((parameter) => parameter.value) }); return binding; },});
export function createAcmeExecutor(client: WireClient): QueryExecutor { return { ownershipKey: client, statementBinding: acmeBinding, async query<Row>( statement: RenderedStatement, binding?: StatementBindingDescription, options?: ExecutionOptions, ): Promise<QueryExecutionResult<Row>> { assertSignal(options); statement = createRenderedStatement(statement); const description = binding ?? acmeBinding.describe(statement, { dialectId: statement.dialectId, requestedReuse: "auto" }); const request = requests.get(description); if (request?.statement !== statement) throw new TypeError("BRAID_BINDING_IDENTITY"); return client.execute<Row>(request.text, request.values); }, stream(_statement, _binding, options): AsyncIterable<never> { assertSignal(options); throw new UnsupportedFeatureError( "statement.stream", "BRAID_STREAM_UNSUPPORTED", "acme-wire has no stream protocol", ); }, async call(_statement, _binding, options): Promise<never> { assertSignal(options); throw new UnsupportedFeatureError("routine.call", "BRAID_CALL_UNSUPPORTED", "acme-wire has no routine protocol"); }, };}
export function createAcmeProvider(acquireClient: () => Promise<WireClient>): ConnectionProvider { return { statementBinding: acmeBinding, async acquire(): Promise<ConnectionLease> { const client = await acquireClient(); const executor = createAcmeExecutor(client); let released = false; return { ...executor, async release(options) { if (released) return; released = true; await client.release(options); }, }; }, };}기능과 근거
섹션 제목: “기능과 근거”트랜잭션 격리 수준은 정해진 리터럴과 readOnly만 쓰세요.
- 형식이 잘못된 런타임 값은
TypeError/BRAID_TX_OPTIONS_INVALID로 실패합니다. - 올바르지만 지원하지 않는 옵션은
BRAID_TX_OPTION_UNSUPPORTED를 씁니다. - 중첩 트랜잭션에 명시적 옵션을 주면
BRAID_TX_OPTIONS_NESTED를 씁니다.
정규 기능 키는 statement.prepare, statement.stream, statement.bulk, transaction, transaction.savepoint, routine.out, routine.result-sets, routine.out-cursor, routine.return-value입니다.
Bun SQL은 어댑터 계열 하나를 씁니다. 사용자는 dialect: "postgres" | "mysql" | "mariadb" | "sqlite"를 직접 골라야 하며, 어댑터가 자동으로 감지하지 않습니다.
- Bun.SQL MySQL과 MariaDB는 명시적인
readOnly값을 둘 다 I/O 전에 거부합니다(BRAID_TX_OPTION_UNSUPPORTED/transaction.read-only). 옵션을 생략하면 네이티브 세션 기본값이 유지됩니다. - Bun 1.3.14에서는 읽기 전용 실패가 롤백 후에도 커넥션에 남을 수 있습니다. 그래서 이렇게 오염된 예약 커넥션은 폐기해야 합니다.
- Bun.SQL PostgreSQL의 접근 모드와 표현 방식 프로필 옵션은 바뀌지 않습니다.
Deno에서는 공개 드라이버 API가 동작하는 범위에서 기존 어댑터를 쓸 수 있습니다. 위의 어느 설명도 검증되지 않은 데이터베이스·런타임·프로필 조합의 등급을 올리지 않습니다. 전체 점검 목록은 저장소의 드라이버 작성자 가이드를 보세요.
SQLite의 경우:
node:sqlite와better-sqlite3는Awaitable을 통해 물리 결과를 동기로 반환할 수 있습니다. 공개 데이터베이스 API는 그래도 비동기입니다. better-sqlite3는 여전히 이벤트 루프를 막습니다.- INTEGER를 정확히 읽으려면 각 문장에
safeIntegers(true)를 쓰세요. 네이티브 반복은 그대로 노출합니다. - libSQL 어댑터는
intMode: "string"을 명시해야 합니다. 대화형 트랜잭션 핸들을 쓰며, 일반 세션 고정은 지원한다고 주장하지 않습니다. 선택한 클라이언트에 점진적 커서가 없으면 스트리밍을 거부해야 합니다. - 로컬 libSQL 근거로 원격 전송 방식이 인증되지는 않습니다.