Created
August 15, 2024 02:51
-
-
Save luixo/102b9fbf6147f5c4f454b407bae4457e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { PGlite } from "@electric-sql/pglite"; | |
| import stringify from "json-stable-stringify"; | |
| import { Kysely, PostgresDialect } from "kysely"; | |
| import type { | |
| PostgresCursor, | |
| PostgresPool, | |
| PostgresPoolClient, | |
| PostgresQueryResult, | |
| } from "kysely"; | |
| // Mostly copy-pasted from https://github.com/oguimbal/pg-mem/blob/master/src/adapters/adapters.ts | |
| type LiteralInput = null | string | Buffer | LiteralInput[]; | |
| const literal = (val: LiteralInput): string => { | |
| if (val === null) { | |
| return "NULL"; | |
| } | |
| if (Array.isArray(val)) { | |
| return `(${val.map(literal).join(", ")})`; | |
| } | |
| const prefix = val.includes("\\") ? "E" : ""; | |
| return `${prefix}'${val | |
| .toString() | |
| .replace(/'/g, "''") | |
| .replace(/\\/g, "\\\\")}'`; | |
| }; | |
| const pad = (number: number, digits: number) => { | |
| let numberStr = String(number); | |
| while (numberStr.length < digits) { | |
| numberStr = `0${numberStr}`; | |
| } | |
| return numberStr; | |
| }; | |
| const dateToString = (date: Date) => { | |
| let offset = -date.getTimezoneOffset(); | |
| let year = date.getFullYear(); | |
| const isBCYear = year < 1; | |
| if (isBCYear) year = Math.abs(year) + 1; // negative years are 1 off their BC representation | |
| let ret = `${pad(year, 4)}-${pad(date.getMonth() + 1, 2)}-${pad( | |
| date.getDate(), | |
| 2, | |
| )}T${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad( | |
| date.getSeconds(), | |
| 2, | |
| )}.${pad(date.getMilliseconds(), 3)}`; | |
| if (offset < 0) { | |
| ret += "-"; | |
| offset *= -1; | |
| } else { | |
| ret += "+"; | |
| } | |
| ret += `${pad(Math.floor(offset / 60), 2)}:${pad(offset % 60, 2)}`; | |
| if (isBCYear) { | |
| ret += " BC"; | |
| } | |
| return ret; | |
| }; | |
| function prepareValue(val: unknown, seen: unknown[] = []): string { | |
| if (val === undefined || val === null) { | |
| return "null"; | |
| } | |
| if (Buffer.isBuffer(val)) { | |
| return literal(Buffer.from(val)); | |
| } | |
| if (val instanceof Date) { | |
| return literal(dateToString(val)); | |
| } | |
| if (Array.isArray(val)) { | |
| if (val.length === 0) { | |
| return `'{}'`; | |
| } | |
| return `ARRAY[${val.map((x) => prepareValue(x)).join(", ")}]`; | |
| } | |
| if (typeof val === "object") { | |
| return prepareObject(val, seen); | |
| } | |
| return literal(val.toString()); | |
| } | |
| function prepareObject(val: object | null, seen: unknown[] = []): string { | |
| if (val && "toPostgres" in val && typeof val.toPostgres === "function") { | |
| if (seen.includes(val)) { | |
| throw new Error( | |
| `circular reference detected while preparing "${String( | |
| val, | |
| )}" for query`, | |
| ); | |
| } | |
| seen.push(val); | |
| return prepareValue(val.toPostgres(prepareValue), seen); | |
| } | |
| const s = stringify(val); | |
| return literal(s); | |
| } | |
| const replaceQueryArgs = (sql: string, values: readonly unknown[]) => | |
| sql.replace(/\$(\d+)/g, (str, istr: string) => { | |
| const i = Number.parseInt(istr, 10); | |
| if (i > values.length) { | |
| throw new Error(`Unmatched parameter in query ${str}`); | |
| } | |
| return prepareValue(values[i - 1]); | |
| }); | |
| class PgLiteClient implements PostgresPoolClient { | |
| constructor(private db: PGlite) {} | |
| query<R>( | |
| sql: string, | |
| parameters: readonly unknown[], | |
| ): Promise<PostgresQueryResult<R>>; | |
| query<R>(cursor: PostgresCursor<R>): PostgresCursor<R>; | |
| query<R>(...args: unknown[]) { | |
| if (args.length === 2) { | |
| const [query, values] = args as [string, readonly unknown[]]; | |
| const queryWithValues = | |
| values.length === 0 ? query : replaceQueryArgs(query, values); | |
| return this.db.query(queryWithValues).then((result) => ({ | |
| // TODO: figure out command | |
| command: result.affectedRows ? "UPDATE" : "SELECT", | |
| rowCount: result.affectedRows ?? 0, | |
| rows: result.rows.map((row) => structuredClone(row)), | |
| })); | |
| } | |
| // cursor is not actually supported | |
| const [cursor] = args as [PostgresCursor<R>]; | |
| return cursor; | |
| } | |
| release() {} | |
| } | |
| export class PgLitePool implements PostgresPool { | |
| constructor(private db: PGlite) {} | |
| async connect() { | |
| return new PgLiteClient(this.db) as PostgresPoolClient; | |
| } | |
| async end() {} | |
| } | |
| // usage | |
| const localDb = new PGlite(); | |
| const database = new Kysely({ | |
| dialect: new PostgresDialect({ | |
| pool: new PgLitePool(localDb) | |
| }), | |
| }) | |
| await sql`SELECT 1`.execute(database); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment