UNPKG

@electric-sql/pglite-tools

Version:

Tools for working with PGlite databases

247 lines (218 loc) 5.92 kB
import { WasiPreview1 } from './wasi/easywasi' import { postgresMod } from '@electric-sql/pglite' import { PGlite } from '@electric-sql/pglite' type FS = postgresMod.FS type FSInterface = any // WASI FS interface const IN_NODE = typeof process === 'object' && typeof process.versions === 'object' && typeof process.versions.node === 'string' /** * Emscripten FS is not quite compatible with WASI * so we need to patch it */ function emscriptenFsToWasiFS(fs: FS, acc: any[]): FSInterface & FS { const requiredMethods = [ 'appendFileSync', 'fsyncSync', 'linkSync', 'setFlagsSync', 'mkdirSync', 'readdirSync', 'readFileSync', 'readlinkSync', 'renameSync', 'rmdirSync', 'statSync', 'symlinkSync', 'truncateSync', 'unlinkSync', 'utimesSync', 'writeFileSync', ] return { // Bind all methods to the FS instance ...fs, // Add missing methods ...Object.fromEntries( requiredMethods .map((method) => { const target = method.slice(0, method.length - 4) if (!(target in fs)) { return [ method, () => { throw new Error(`${target} not implemented.`) }, ] } return [ method, (...args: any[]) => { if (method === 'writeFileSync' && args[0] === '/tmp/out.sql') { acc.push(args[1]) } return (fs as any)[target](...args) }, ] }) .filter( (entry): entry is [string, (fs: FS) => any] => entry !== undefined, ), ), } as FSInterface & FS } /** * Inner function to execute pg_dump */ async function execPgDump({ pg, args, }: { pg: PGlite args: string[] }): Promise<[number, Uint8Array[], string]> { const bin = new URL('./pg_dump.wasm', import.meta.url) const acc: Uint8Array[] = [] const FS = emscriptenFsToWasiFS(pg.Module.FS, acc) const wasi = new WasiPreview1({ fs: FS, args: ['pg_dump', ...args], env: { PWD: '/', }, }) wasi.stdout = (_buffer) => { // console.log('stdout', _buffer) } const textDecoder = new TextDecoder() let errorMessage = '' wasi.stderr = (_buffer) => { const text = textDecoder.decode(_buffer) if (text) errorMessage += text } wasi.sched_yield = () => { const pgIn = '/tmp/pglite/base/.s.PGSQL.5432.in' const pgOut = '/tmp/pglite/base/.s.PGSQL.5432.out' if (FS.analyzePath(pgIn).exists) { // call interactive one const msgIn = FS.readFileSync(pgIn) // BYPASS the file socket emulation in PGlite FS.unlinkSync(pgIn) // Handle auth request if (msgIn[0] === 0) { const reply = new Uint8Array([ ...authOk, ...versionParam, ...readyForQuery, ]) FS.writeFileSync(pgOut, reply) return 0 } // Handle query const reply = pg.execProtocolRawSync(msgIn) FS.writeFileSync(pgOut, reply) } return 0 } // Postgres can complain if the binary is not on the filesystem // so we create a dummy file await FS.writeFile('/pg_dump', '\0', { mode: 18 }) let app: WebAssembly.WebAssemblyInstantiatedSource if (IN_NODE) { const fs = await import('fs/promises') const blob = await fs.readFile(bin.toString().slice(7)) app = await WebAssembly.instantiate(blob, { wasi_snapshot_preview1: wasi as any, }) } else { app = await WebAssembly.instantiateStreaming(fetch(bin), { wasi_snapshot_preview1: wasi as any, }) } let exitCode: number await pg.runExclusive(async () => { exitCode = wasi.start(app.instance.exports) }) return [exitCode!, acc, errorMessage] } interface PgDumpOptions { pg: PGlite args?: string[] fileName?: string } /** * Execute pg_dump */ export async function pgDump({ pg, args, fileName = 'dump.sql', }: PgDumpOptions) { const getSearchPath = await pg.query<{ search_path: string }>( 'SHOW SEARCH_PATH;', ) const search_path = getSearchPath.rows[0].search_path const outFile = `/tmp/out.sql` const baseArgs = [ '-U', 'postgres', '--inserts', '-j', '1', '-f', outFile, 'postgres', ] const [exitCode, acc, errorMessage] = await execPgDump({ pg, args: [...(args ?? []), ...baseArgs], }) pg.exec(`DEALLOCATE ALL; SET SEARCH_PATH = ${search_path}`) if (exitCode !== 0) { throw new Error( `pg_dump failed with exit code ${exitCode}. \nError message: ${errorMessage}`, ) } const file = new File(acc, fileName, { type: 'text/plain', }) pg.Module.FS.unlink(outFile) return file } // Wire protocol messages for simulating auth handshake: function charToByte(char: string) { return char.charCodeAt(0) } // Function to convert an integer to a 4-byte array (Int32) function int32ToBytes(value: number) { const buffer = new ArrayBuffer(4) const view = new DataView(buffer) view.setInt32(0, value, false) // false for big-endian return new Uint8Array(buffer) } // Convert a string to a Uint8Array with a null terminator (C string) function stringToBytes(str: string) { const utf8Encoder = new TextEncoder() const strBytes = utf8Encoder.encode(str) // UTF-8 encoding return new Uint8Array([...strBytes, 0]) // Append null terminator } const authOk = new Uint8Array([ charToByte('R'), ...int32ToBytes(8), ...int32ToBytes(0), ]) const readyForQuery = new Uint8Array([ charToByte('Z'), ...int32ToBytes(5), charToByte('I'), ]) const svParamName = stringToBytes('server_version') const svParamValue = stringToBytes('16.3 (PGlite 0.2.0)') const svTotalLength = 4 + svParamName.length + svParamValue.length const versionParam = new Uint8Array([ charToByte('S'), ...int32ToBytes(svTotalLength), ...svParamName, ...svParamValue, ])