pglite-prisma-adapter
Version:
Prisma's driver adapter for "@electric-sql/pglite"
581 lines (575 loc) • 17.3 kB
JavaScript
import * as pglite from "@electric-sql/pglite";
import { types } from "@electric-sql/pglite";
import { ColumnTypeEnum, Debug, DriverAdapterError } from "@prisma/driver-adapter-utils";
import { parse } from "postgres-array";
//#region package.json
var name = "pglite-prisma-adapter";
//#endregion
//#region src/conversion.ts
/**
* Additional scalar column types not defined in `pg` types builtins.
*/
const AdditionalScalarColumnType = { NAME: 19 };
const ScalarColumnType = types;
/**
* PostgreSQL array column types (not defined in ScalarColumnType).
*
* See the semantics of each of this code in:
* https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
*/
const ArrayColumnType = {
BIT: 1561,
BOOL: 1e3,
BYTEA: 1001,
BPCHAR: 1014,
CHAR: 1002,
CIDR: 651,
DATE: 1182,
FLOAT4: 1021,
FLOAT8: 1022,
INET: 1041,
INT2: 1005,
INT4: 1007,
INT8: 1016,
JSONB: 3807,
JSON: 199,
MONEY: 791,
NUMERIC: 1231,
OID: 1028,
TEXT: 1009,
TIMESTAMP: 1115,
TIMESTAMPTZ: 1185,
TIME: 1183,
UUID: 2951,
VARBIT: 1563,
VARCHAR: 1015,
XML: 143
};
var UnsupportedNativeDataType = class UnsupportedNativeDataType extends Error {
static typeNames = {
16: "bool",
17: "bytea",
18: "char",
19: "name",
20: "int8",
21: "int2",
22: "int2vector",
23: "int4",
24: "regproc",
25: "text",
26: "oid",
27: "tid",
28: "xid",
29: "cid",
30: "oidvector",
32: "pg_ddl_command",
71: "pg_type",
75: "pg_attribute",
81: "pg_proc",
83: "pg_class",
114: "json",
142: "xml",
194: "pg_node_tree",
269: "table_am_handler",
325: "index_am_handler",
600: "point",
601: "lseg",
602: "path",
603: "box",
604: "polygon",
628: "line",
650: "cidr",
700: "float4",
701: "float8",
705: "unknown",
718: "circle",
774: "macaddr8",
790: "money",
829: "macaddr",
869: "inet",
1033: "aclitem",
1042: "bpchar",
1043: "varchar",
1082: "date",
1083: "time",
1114: "timestamp",
1184: "timestamptz",
1186: "interval",
1266: "timetz",
1560: "bit",
1562: "varbit",
1700: "numeric",
1790: "refcursor",
2202: "regprocedure",
2203: "regoper",
2204: "regoperator",
2205: "regclass",
2206: "regtype",
2249: "record",
2275: "cstring",
2276: "any",
2277: "anyarray",
2278: "void",
2279: "trigger",
2280: "language_handler",
2281: "internal",
2283: "anyelement",
2287: "_record",
2776: "anynonarray",
2950: "uuid",
2970: "txid_snapshot",
3115: "fdw_handler",
3220: "pg_lsn",
3310: "tsm_handler",
3361: "pg_ndistinct",
3402: "pg_dependencies",
3500: "anyenum",
3614: "tsvector",
3615: "tsquery",
3642: "gtsvector",
3734: "regconfig",
3769: "regdictionary",
3802: "jsonb",
3831: "anyrange",
3838: "event_trigger",
3904: "int4range",
3906: "numrange",
3908: "tsrange",
3910: "tstzrange",
3912: "daterange",
3926: "int8range",
4072: "jsonpath",
4089: "regnamespace",
4096: "regrole",
4191: "regcollation",
4451: "int4multirange",
4532: "nummultirange",
4533: "tsmultirange",
4534: "tstzmultirange",
4535: "datemultirange",
4536: "int8multirange",
4537: "anymultirange",
4538: "anycompatiblemultirange",
4600: "pg_brin_bloom_summary",
4601: "pg_brin_minmax_multi_summary",
5017: "pg_mcv_list",
5038: "pg_snapshot",
5069: "xid8",
5077: "anycompatible",
5078: "anycompatiblearray",
5079: "anycompatiblenonarray",
5080: "anycompatiblerange"
};
type;
constructor(code) {
super();
this.type = UnsupportedNativeDataType.typeNames[code] || "Unknown";
this.message = `Unsupported column type ${this.type}`;
}
};
/**
* This is a simplification of quaint's value inference logic. Take a look at quaint's conversion.rs
* module to see how other attributes of the field packet such as the field length are used to infer
* the correct quaint::Value variant.
*/
function fieldToColumnType(fieldTypeId) {
switch (fieldTypeId) {
case ScalarColumnType.INT2:
case ScalarColumnType.INT4: return ColumnTypeEnum.Int32;
case ScalarColumnType.INT8: return ColumnTypeEnum.Int64;
case ScalarColumnType.FLOAT4: return ColumnTypeEnum.Float;
case ScalarColumnType.FLOAT8: return ColumnTypeEnum.Double;
case ScalarColumnType.BOOL: return ColumnTypeEnum.Boolean;
case ScalarColumnType.DATE: return ColumnTypeEnum.Date;
case ScalarColumnType.TIME:
case ScalarColumnType.TIMETZ: return ColumnTypeEnum.Time;
case ScalarColumnType.TIMESTAMP:
case ScalarColumnType.TIMESTAMPTZ: return ColumnTypeEnum.DateTime;
case ScalarColumnType.NUMERIC:
case ScalarColumnType.MONEY: return ColumnTypeEnum.Numeric;
case ScalarColumnType.JSON:
case ScalarColumnType.JSONB: return ColumnTypeEnum.Json;
case ScalarColumnType.UUID: return ColumnTypeEnum.Uuid;
case ScalarColumnType.OID: return ColumnTypeEnum.Int64;
case ScalarColumnType.BPCHAR:
case ScalarColumnType.TEXT:
case ScalarColumnType.VARCHAR:
case ScalarColumnType.BIT:
case ScalarColumnType.VARBIT:
case ScalarColumnType.INET:
case ScalarColumnType.CIDR:
case ScalarColumnType.XML:
case AdditionalScalarColumnType.NAME: return ColumnTypeEnum.Text;
case ScalarColumnType.CHAR: return ColumnTypeEnum.Character;
case ScalarColumnType.BYTEA: return ColumnTypeEnum.Bytes;
case ArrayColumnType.INT2:
case ArrayColumnType.INT4: return ColumnTypeEnum.Int32Array;
case ArrayColumnType.FLOAT4: return ColumnTypeEnum.FloatArray;
case ArrayColumnType.FLOAT8: return ColumnTypeEnum.DoubleArray;
case ArrayColumnType.NUMERIC:
case ArrayColumnType.MONEY: return ColumnTypeEnum.NumericArray;
case ArrayColumnType.BOOL: return ColumnTypeEnum.BooleanArray;
case ArrayColumnType.CHAR: return ColumnTypeEnum.CharacterArray;
case ArrayColumnType.BPCHAR:
case ArrayColumnType.TEXT:
case ArrayColumnType.VARCHAR:
case ArrayColumnType.VARBIT:
case ArrayColumnType.BIT:
case ArrayColumnType.INET:
case ArrayColumnType.CIDR:
case ArrayColumnType.XML: return ColumnTypeEnum.TextArray;
case ArrayColumnType.DATE: return ColumnTypeEnum.DateArray;
case ArrayColumnType.TIMESTAMP:
case ArrayColumnType.TIMESTAMPTZ: return ColumnTypeEnum.DateTimeArray;
case ArrayColumnType.JSON:
case ArrayColumnType.JSONB: return ColumnTypeEnum.JsonArray;
case ArrayColumnType.BYTEA: return ColumnTypeEnum.BytesArray;
case ArrayColumnType.UUID: return ColumnTypeEnum.UuidArray;
case ArrayColumnType.INT8:
case ArrayColumnType.OID: return ColumnTypeEnum.Int64Array;
default:
if (fieldTypeId >= 1e4) return ColumnTypeEnum.Text;
throw new UnsupportedNativeDataType(fieldTypeId);
}
}
function normalize_array(element_normalizer) {
return (str) => parse(str, element_normalizer);
}
function normalize_numeric(numeric) {
return numeric;
}
function normalize_date(date) {
return date;
}
function normalize_timestamp(time) {
return `${time.replace(" ", "T")}+00:00`;
}
function normalize_timestampz(time) {
return time.replace(" ", "T").replace(/[+-]\d{2}(:\d{2})?$/, "+00:00");
}
function normalize_time(time) {
return time;
}
function normalize_timez(time) {
return time.replace(/[+-]\d{2}(:\d{2})?$/, "");
}
function normalize_money(money) {
return money.slice(1);
}
function normalize_xml(xml) {
return xml;
}
/**
* We hand off JSON handling entirely to engines, so we keep it
* stringified here. This function needs to exist as otherwise
* the default type parser attempts to deserialise it.
*/
function toJson(json) {
return json;
}
const parsePgBytes = (x) => {
const hexString = x.slice(2);
return Uint8Array.from({ length: hexString.length / 2 }, (_, idx) => Number.parseInt(hexString.substring(idx * 2, (idx + 1) * 2), 16));
};
function normalizeByteaArray(x) {
return parse(x).map((x$1) => {
const hexString = x$1.slice(2);
return Uint8Array.from({ length: hexString.length / 2 }, (_, idx) => Number.parseInt(hexString.substring(idx * 2, (idx + 1) * 2), 16));
});
}
/**
* Convert bytes to a JSON-encodable representation since we can't
* currently send a parsed Buffer or ArrayBuffer across JS to Rust
* boundary.
*/
function convertBytes(serializedBytes) {
return parsePgBytes(serializedBytes);
}
function normalizeBit(bit) {
return bit;
}
function normalizeBigInt(bigint) {
return bigint;
}
const customParsers = {
[ScalarColumnType.NUMERIC]: normalize_numeric,
[ArrayColumnType.NUMERIC]: normalize_array(normalize_numeric),
[ScalarColumnType.TIME]: normalize_time,
[ArrayColumnType.TIME]: normalize_array(normalize_time),
[ScalarColumnType.TIMETZ]: normalize_timez,
[ScalarColumnType.DATE]: normalize_date,
[ArrayColumnType.DATE]: normalize_array(normalize_date),
[ScalarColumnType.TIMESTAMP]: normalize_timestamp,
[ArrayColumnType.TIMESTAMP]: normalize_array(normalize_timestamp),
[ScalarColumnType.TIMESTAMPTZ]: normalize_timestampz,
[ArrayColumnType.TIMESTAMPTZ]: normalize_array(normalize_timestampz),
[ScalarColumnType.MONEY]: normalize_money,
[ArrayColumnType.MONEY]: normalize_array(normalize_money),
[ScalarColumnType.JSON]: toJson,
[ArrayColumnType.JSON]: normalize_array(toJson),
[ScalarColumnType.JSONB]: toJson,
[ArrayColumnType.JSONB]: normalize_array(toJson),
[ScalarColumnType.BYTEA]: convertBytes,
[ArrayColumnType.BYTEA]: normalizeByteaArray,
[ArrayColumnType.BIT]: normalize_array(normalizeBit),
[ArrayColumnType.VARBIT]: normalize_array(normalizeBit),
[ArrayColumnType.XML]: normalize_array(normalize_xml),
[ScalarColumnType.INT8]: normalizeBigInt,
[ArrayColumnType.INT8]: normalize_array(normalizeBigInt)
};
function mapArg(arg, argType) {
if (arg === null) return null;
if (Array.isArray(arg) && argType.arity === "list") return arg.map((value) => mapArg(value, argType));
if (typeof arg === "string" && argType.scalarType === "datetime") arg = new Date(arg);
if (arg instanceof Date) switch (argType.dbType) {
case "TIME":
case "TIMETZ": return formatTime(arg);
case "DATE": return formatDate(arg);
default: return formatDateTime(arg);
}
if (typeof arg === "string" && argType.scalarType === "bytes") return Buffer.from(arg, "base64");
if (ArrayBuffer.isView(arg)) return new Uint8Array(arg.buffer, arg.byteOffset, arg.byteLength);
return arg;
}
function formatDateTime(date) {
const pad = (n, z = 2) => String(n).padStart(z, "0");
const ms = date.getUTCMilliseconds();
return `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}${ms ? `.${String(ms).padStart(3, "0")}` : ""}`;
}
function formatDate(date) {
const pad = (n, z = 2) => String(n).padStart(z, "0");
return `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
}
function formatTime(date) {
const pad = (n, z = 2) => String(n).padStart(z, "0");
const ms = date.getUTCMilliseconds();
return `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}${ms ? `.${String(ms).padStart(3, "0")}` : ""}`;
}
//#endregion
//#region src/deferred.ts
function createDeferred() {
const deferred = {};
return [deferred, new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
})];
}
//#endregion
//#region src/errors.ts
function convertDriverError(error) {
if (isDriverError(error)) return {
originalCode: error.code,
originalMessage: error.message,
...mapDriverError(error)
};
throw error;
}
function mapDriverError(error) {
switch (error.code) {
case "22001": return {
kind: "LengthMismatch",
column: error.column
};
case "22003": return {
kind: "ValueOutOfRange",
cause: error.message
};
case "22P02": return {
kind: "InvalidInputValue",
message: error.message
};
case "23505": {
const fields = error.detail?.match(/Key \(([^)]+)\)/)?.at(1)?.split(", ");
return {
kind: "UniqueConstraintViolation",
constraint: fields !== void 0 ? { fields } : void 0
};
}
case "23502": {
const fields = error.detail?.match(/Key \(([^)]+)\)/)?.at(1)?.split(", ");
return {
kind: "NullConstraintViolation",
constraint: fields !== void 0 ? { fields } : void 0
};
}
case "23503": {
let constraint;
if (error.column) constraint = { fields: [error.column] };
else if (error.constraint) constraint = { index: error.constraint };
return {
kind: "ForeignKeyConstraintViolation",
constraint
};
}
case "3D000": return {
kind: "DatabaseDoesNotExist",
db: error.message.split(" ").at(1)?.split("\"").at(1)
};
case "28000": return {
kind: "DatabaseAccessDenied",
db: error.message.split(",").find((s) => s.startsWith(" database"))?.split("\"").at(1)
};
case "28P01": return {
kind: "AuthenticationFailed",
user: error.message.split(" ").pop()?.split("\"").at(1)
};
case "40001": return { kind: "TransactionWriteConflict" };
case "42P01": return {
kind: "TableDoesNotExist",
table: error.message.split(" ").at(1)?.split("\"").at(1)
};
case "42703": return {
kind: "ColumnNotFound",
column: error.message.split(" ").at(1)?.split("\"").at(1)
};
case "42P04": return {
kind: "DatabaseAlreadyExists",
db: error.message.split(" ").at(1)?.split("\"").at(1)
};
case "53300": return {
kind: "TooManyConnections",
cause: error.message
};
default: return {
kind: "postgres",
code: error.code ?? "N/A",
severity: error.severity ?? "N/A",
message: error.message,
detail: error.detail,
column: error.column,
hint: error.hint
};
}
}
function isDriverError(error) {
return typeof error.code === "string" && typeof error.message === "string" && typeof error.severity === "string" && (typeof error.detail === "string" || error.detail === void 0) && (typeof error.column === "string" || error.column === void 0) && (typeof error.hint === "string" || error.hint === void 0);
}
//#endregion
//#region src/pglite.ts
const debug = Debug("prisma:driver-adapter:pglite");
var PGliteQueryable = class {
provider = "postgres";
adapterName = name;
constructor(client) {
this.client = client;
}
async queryRaw(query) {
debug(`[js::query_raw] %O`, query);
const { fields, rows } = await this.performIO(query);
const columnNames = fields.map((field) => field.name);
let columnTypes = [];
try {
columnTypes = fields.map((field) => fieldToColumnType(field.dataTypeID));
} catch (e) {
if (e instanceof UnsupportedNativeDataType) throw new DriverAdapterError({
kind: "UnsupportedNativeDataType",
type: e.type
});
throw e;
}
return {
columnNames,
columnTypes,
rows
};
}
/**
* Execute a query given as SQL, interpolating the given parameters and
* returning the number of affected rows.
*/
async executeRaw(query) {
debug(`[js::execute_raw] %O`, query);
return (await this.performIO(query)).affectedRows ?? 0;
}
async performIO(query) {
const { sql, args } = query;
const values = args.map((arg, i) => mapArg(arg, query.argTypes[i]));
try {
return await this.client.query(sql, values, {
rowMode: "array",
parsers: customParsers
});
} catch (e) {
this.onError(e);
}
}
onError(error) {
debug("Error in performIO: %O", error);
if (error instanceof pglite.messages.DatabaseError) throw new DriverAdapterError(convertDriverError(error));
throw error;
}
};
var PGliteTransaction = class extends PGliteQueryable {
constructor(client, options, txDeferred, txResultPromise) {
super(client);
this.options = options;
this.txDeferred = txDeferred;
this.txResultPromise = txResultPromise;
}
async commit() {
debug("[js::commit]");
this.txDeferred.resolve();
return await this.txResultPromise;
}
async rollback() {
debug("[js::rollback]");
this.client.rollback();
this.txDeferred.resolve();
return await this.txResultPromise;
}
};
var PrismaPGliteAdapter = class extends PGliteQueryable {
constructor(client, options) {
super(client);
this.options = options;
}
executeScript(script) {
try {
this.client.exec(script);
} catch (e) {
this.onError(e);
}
return Promise.resolve();
}
getConnectionInfo() {
return {
schemaName: this.options?.schema,
supportsRelationJoins: true
};
}
async startTransaction(isolationLevel) {
const options = { usePhantomQuery: true };
debug("%s options: %O", "[js::startTransaction]", options);
if (isolationLevel) await this.client.exec(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}`).catch((error) => this.onError(error));
return this.startTransactionInner(this.client, options);
}
async startTransactionInner(conn, options) {
return new Promise((resolve, reject) => {
const txResultPromise = conn.transaction(async (tx) => {
const [txDeferred, deferredPromise] = createDeferred();
resolve(new PGliteTransaction(tx, options, txDeferred, txResultPromise));
return deferredPromise;
}).catch((error) => {
return reject(error);
});
});
}
async dispose() {
return Promise.resolve();
}
};
var PrismaPGliteAdapterFactory = class {
provider = "postgres";
adapterName = name;
constructor(client) {
this.client = client;
}
connect() {
return Promise.resolve(new PrismaPGliteAdapter(this.client));
}
connectToShadowDb() {
return Promise.resolve(new PrismaPGliteAdapter(new pglite.PGlite({ dataDir: "memory://shadow" })));
}
};
//#endregion
export { PrismaPGliteAdapterFactory as PrismaPGlite };