2025-01-27 15:53:20 +03:00

150 lines
4.1 KiB
TypeScript

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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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)})`;
}
}