import type { Database } from "./database.ts"; import ffi from "./ffi.ts"; import { toCString, unwrap } from "./util.ts"; const { sqlite3_blob_open, sqlite3_blob_bytes, sqlite3_blob_close, sqlite3_blob_read, sqlite3_blob_write, } = ffi; /** Various options that can be configured when opening a Blob via `Database#openBlob`. */ export interface BlobOpenOptions { /** Whether to open Blob in readonly mode. True by default. */ readonly?: boolean; /** Database to open Blob from, "main" by default. */ db?: string; /** Table the Blob is in */ table: string; /** Column name of the Blob Field */ column: string; /** Row ID of which column to select */ row: number; } /** * Enumerates SQLite3 Blob opened for streamed I/O. * * BLOB columns still return a `Uint8Array` of the data. * You can instead open this from `Database.openBlob()`. * * @see https://www.sqlite.org/c3ref/blob_open.html */ export class SQLBlob { #handle: Deno.PointerValue; constructor(db: Database, options: BlobOpenOptions) { options = Object.assign({ readonly: true, db: "main", }, options); const pHandle = new BigUint64Array(1); unwrap(sqlite3_blob_open( db.unsafeHandle, toCString(options.db ?? "main"), toCString(options.table), toCString(options.column), BigInt(options.row), options.readonly === false ? 1 : 0, pHandle, )); this.#handle = Deno.UnsafePointer.create(pHandle[0]); } /** Byte size of the Blob */ get byteLength(): number { return sqlite3_blob_bytes(this.#handle); } /** Read from the Blob at given offset into a buffer (Uint8Array) */ readSync(offset: number, p: Uint8Array): void { unwrap(sqlite3_blob_read(this.#handle, p, p.byteLength, offset)); } /** Write a buffer (Uint8Array) at given offset in the Blob */ writeSync(offset: number, p: Uint8Array): void { unwrap(sqlite3_blob_write(this.#handle, p, p.byteLength, offset)); } /** Close the Blob. It **must** be called to prevent leaks. */ close(): void { unwrap(sqlite3_blob_close(this.#handle)); } /** Obtains Web Stream for reading the Blob */ get readable(): ReadableStream { const length = this.byteLength; let offset = 0; return new ReadableStream({ type: "bytes", pull: (ctx) => { try { const byob = ctx.byobRequest; if (byob) { const toRead = Math.min( length - offset, byob.view!.byteLength, ); this.readSync( offset, (byob.view as Uint8Array).subarray(0, toRead), ); offset += toRead; byob.respond(toRead); } else { const toRead = Math.min( length - offset, ctx.desiredSize || 1024 * 16, ); if (toRead === 0) { ctx.close(); return; } const buffer = new Uint8Array(toRead); this.readSync(offset, buffer); offset += toRead; ctx.enqueue(buffer); } } catch (e) { ctx.error(e); ctx.byobRequest?.respond(0); } }, }); } /** Obtains Web Stream for writing to the Blob */ get writable(): WritableStream { const length = this.byteLength; let offset = 0; return new WritableStream({ write: (chunk, ctx) => { if (offset + chunk.byteLength > length) { ctx.error(new Error("Write exceeds blob length")); return; } this.writeSync(offset, chunk); offset += chunk.byteLength; }, }); } *[Symbol.iterator](): IterableIterator { const length = this.byteLength; let offset = 0; while (offset < length) { const toRead = Math.min(length - offset, 1024 * 16); const buffer = new Uint8Array(toRead); this.readSync(offset, buffer); offset += toRead; yield buffer; } } [Symbol.for("Deno.customInspect")](): string { return `SQLite3.Blob(0x${this.byteLength.toString(16)})`; } }