@uwdata/mosaic-core
Version:
Scalable and extensible linked data views.
146 lines (131 loc) • 4.63 kB
text/typescript
import type { ExtractionOptions, Table } from '@uwdata/flechette';
import type { ArrowQueryRequest, Connector, ExecQueryRequest, JSONQueryRequest, ConnectorQueryRequest } from './Connector.js';
import * as duckdb from '@duckdb/duckdb-wasm';
import { decodeIPC } from '../util/decode-ipc.js';
interface DuckDBWASMOptions {
/** Flag to enable logging. */
log?: boolean;
}
interface DuckDBWASMConnectorOptions extends DuckDBWASMOptions {
/** Arrow IPC extraction options. */
ipc?: ExtractionOptions;
/** Optional pre-existing DuckDB-WASM instance. */
duckdb?: duckdb.AsyncDuckDB;
/** Optional pre-existing DuckDB-WASM connection. */
connection?: duckdb.AsyncDuckDBConnection;
}
/**
* Connect to a DuckDB-WASM instance.
* @param options Connector options.
* @returns A connector instance.
*/
export function wasmConnector(options: DuckDBWASMConnectorOptions = {}): DuckDBWASMConnector {
return new DuckDBWASMConnector(options);
}
/**
* DuckDB-WASM connector.
*/
export class DuckDBWASMConnector implements Connector {
private _ipc?: ExtractionOptions;
public _options: DuckDBWASMOptions;
public _db?: duckdb.AsyncDuckDB;
public _con?: duckdb.AsyncDuckDBConnection;
public _loadPromise?: Promise<unknown>;
/**
* Create a new DuckDB-WASM connector instance.
* @param options Connector options.
*/
constructor(options: DuckDBWASMConnectorOptions = {}) {
const { ipc, duckdb, connection, ...opts } = options;
this._ipc = ipc;
this._options = opts;
this._db = duckdb;
this._con = connection;
}
/**
* Get the backing DuckDB-WASM instance.
* Lazily initializes DuckDB-WASM if not already loaded.
* @returns The DuckDB-WASM instance.
*/
async getDuckDB(): Promise<duckdb.AsyncDuckDB> {
if (!this._db) await connect(this);
return this._db!;
}
/**
* Get the backing DuckDB-WASM connection.
* Lazily initializes DuckDB-WASM if not already loaded.
* @returns The DuckDB-WASM connection.
*/
async getConnection(): Promise<duckdb.AsyncDuckDBConnection> {
if (!this._con) await connect(this);
return this._con!;
}
async query(query: ArrowQueryRequest): Promise<Table>;
async query(query: ExecQueryRequest): Promise<void>;
async query(query: JSONQueryRequest): Promise<Record<string, unknown>[]>;
async query(query: ConnectorQueryRequest): Promise<unknown> {
const { type, sql } = query;
const con = await this.getConnection();
const result = await getArrowIPC(con, sql);
return type === 'exec' ? undefined
: type === 'arrow' ? decodeIPC(result, this._ipc)
: decodeIPC(result).toArray();
}
}
/**
* Bypass duckdb-wasm query method to get Arrow IPC bytes directly.
* https://github.com/duckdb/duckdb-wasm/issues/267#issuecomment-2252749509
* @param con The DuckDB-WASM connection.
* @param query The SQL query to run.
*/
function getArrowIPC(con: duckdb.AsyncDuckDBConnection, query: string): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
con.useUnsafe(async (bindings, conn) => {
try {
const buffer = await bindings.runQuery(conn, query);
resolve(buffer);
} catch (error) {
reject(error);
}
});
});
}
/**
* Establish a new database connection for the given connector.
* @param c The connector.
* @returns Connection promise.
*/
function connect(c: DuckDBWASMConnector): Promise<unknown> {
if (!c._loadPromise) {
// use a loading promise to avoid race conditions
// synchronizes multiple callees on the same load
c._loadPromise = (
c._db
? Promise.resolve(c._db)
: initDatabase(c._options).then(result => c._db = result))
.then(db => db.connect())
.then(result => c._con = result);
}
return c._loadPromise;
}
/**
* Initialize a new DuckDB-WASM instance.
* @param options Database initialization options.
*/
async function initDatabase({
log = false
}: DuckDBWASMOptions = {}): Promise<duckdb.AsyncDuckDB> {
const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
// Select a bundle based on browser checks
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);
const worker_url = URL.createObjectURL(
new Blob([`importScripts("${bundle.mainWorker}");`], {type: 'text/javascript'})
);
// Instantiate the asynchronus version of DuckDB-wasm
const worker = new Worker(worker_url);
const logger = log ? new duckdb.ConsoleLogger() : new duckdb.VoidLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
URL.revokeObjectURL(worker_url);
return db;
}