From 1f483c241349c7bc0d3509d3deefe54dfa551044 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Tue, 12 May 2026 02:08:07 +0700 Subject: [PATCH] feat: add pglite adapter PGlite is a wasm version of postgres database. Dialect is the same just slightly different API. See https://pglite.dev/ --- packages/contrail/package.json | 8 +++ packages/contrail/src/adapters/pglite.ts | 85 ++++++++++++++++++++++++ packages/contrail/tsup.config.ts | 3 +- pnpm-lock.yaml | 8 +++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 packages/contrail/src/adapters/pglite.ts diff --git a/packages/contrail/package.json b/packages/contrail/package.json index 3245e17..220e622 100644 --- a/packages/contrail/package.json +++ b/packages/contrail/package.json @@ -27,6 +27,10 @@ "types": "./dist/adapters/postgres.d.ts", "import": "./dist/adapters/postgres.js" }, + "./pglite": { + "types": "./dist/adapters/pglite.d.ts", + "import": "./dist/adapters/pglite.js" + }, "./workers": { "types": "./dist/workers/backfill.d.ts", "import": "./dist/workers/backfill.js" @@ -89,12 +93,16 @@ }, "peerDependencies": { "pg": "^8.0.0", + "@electric-sql/pglite": "^0.2.0", "wrangler": "^4.0.0" }, "peerDependenciesMeta": { "pg": { "optional": true }, + "@electric-sql/pglite": { + "optional": true + }, "wrangler": { "optional": true } diff --git a/packages/contrail/src/adapters/pglite.ts b/packages/contrail/src/adapters/pglite.ts new file mode 100644 index 0000000..d67fb26 --- /dev/null +++ b/packages/contrail/src/adapters/pglite.ts @@ -0,0 +1,85 @@ +import type { PGlite } from "@electric-sql/pglite"; +import type { Database, Statement } from "../core/types"; +import { postgresDialect } from "../core/dialect"; + +/** Internal interface for statements that can run on a specific transaction */ +interface PgLiteStatement extends Statement { + /** Execute on a specific transaction (used by batch for transaction isolation) */ + _runOn(tx: any): Promise; +} + +/** Column names known to be BIGINT — PostgreSQL returns these as strings */ +const BIGINT_COLUMNS = new Set(["time_us", "indexed_at", "resolved_at"]); + +function normalizeRow(row: any): any { + if (!row) return row; + if (typeof row.record === "object" && row.record !== null) { + row.record = JSON.stringify(row.record); + } + for (const col of BIGINT_COLUMNS) { + if (typeof row[col] === "string") row[col] = Number(row[col]); + } + return row; +} + +export function createPgliteDatabase(pglite: PGlite): Database { + function rewritePlaceholders(sql: string): string { + let idx = 0; + let inString = false; + let result = ""; + for (let i = 0; i < sql.length; i++) { + const ch = sql[i]; + if (ch === "'" && sql[i - 1] !== "\\") { + inString = !inString; + result += ch; + } else if (ch === "?" && !inString) { + result += `$${++idx}`; + } else { + result += ch; + } + } + return result; + } + + function wrapStatement(sql: string, boundValues: any[] = []): PgLiteStatement { + const pgSql = rewritePlaceholders(sql); + + return { + bind(...values: any[]): PgLiteStatement { + return wrapStatement(sql, values); + }, + async run() { + const result = await pglite.query(pgSql, boundValues); + return { changes: result.rows.length }; + }, + async _runOn(tx: any) { + const result = await tx.query(pgSql, boundValues); + return { changes: result.rows.length }; + }, + async all() { + const result = await pglite.query(pgSql, boundValues); + return { results: result.rows.map(normalizeRow) as T[] }; + }, + async first() { + const result = await pglite.query(pgSql, boundValues); + return result.rows[0] ? (normalizeRow(result.rows[0]) as T) : null; + }, + }; + } + + return { + prepare(sql: string): Statement { + return wrapStatement(sql); + }, + async batch(stmts: Statement[]): Promise { + const results: any[] = []; + await pglite.transaction(async (tx) => { + for (const stmt of stmts) { + results.push(await (stmt as PgLiteStatement)._runOn(tx)); + } + }); + return results; + }, + dialect: postgresDialect, + }; +} diff --git a/packages/contrail/tsup.config.ts b/packages/contrail/tsup.config.ts index 7cd50c0..0a74260 100644 --- a/packages/contrail/tsup.config.ts +++ b/packages/contrail/tsup.config.ts @@ -6,6 +6,7 @@ export default defineConfig({ "src/server.ts", "src/adapters/sqlite.ts", "src/adapters/postgres.ts", + "src/adapters/pglite.ts", "src/workers/backfill.ts", "src/worker/index.ts", "src/cli.ts", @@ -16,5 +17,5 @@ export default defineConfig({ sourcemap: true, clean: true, tsconfig: "tsconfig.build.json", - external: ["node:sqlite", "pg", "wrangler"], + external: ["node:sqlite", "pg", "@electric-sql/pglite", "wrangler"], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e648bf..c5086c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: '@atcute/xrpc-server': specifier: ^0.1.12 version: 0.1.12 + '@electric-sql/pglite': + specifier: ^0.2.0 + version: 0.2.17 cac: specifier: ^7.0.0 version: 7.0.0 @@ -647,6 +650,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@electric-sql/pglite@0.2.17': + resolution: {integrity: sha512-qEpKRT2oUaWDH6tjRxLHjdzMqRUGYDnGZlKrnL4dJ77JVMcP2Hpo3NYnOSPKdZdeec57B6QPprCUFg0picx5Pw==} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -3972,6 +3978,8 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@electric-sql/pglite@0.2.17': {} + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1