Skip to content

Instantly share code, notes, and snippets.

@luixo
Created August 15, 2024 02:51
Show Gist options
  • Select an option

  • Save luixo/102b9fbf6147f5c4f454b407bae4457e to your computer and use it in GitHub Desktop.

Select an option

Save luixo/102b9fbf6147f5c4f454b407bae4457e to your computer and use it in GitHub Desktop.
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