Skip to content

Commit

Permalink
Improve type parsing (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
samwillis authored Mar 28, 2024
1 parent b502744 commit d056892
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 23 deletions.
13 changes: 11 additions & 2 deletions packages/pglite/src/pglite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ export class PGlite implements PGliteInterface {
}
await this.#runExec(`
SET search_path TO public;
CREATE EXTENSION IF NOT EXISTS plpgsql;
`);
}

Expand All @@ -166,6 +165,10 @@ export class PGlite implements PGliteInterface {
await this.#runExec(sql);
}
}
await this.#runExec(`
SET search_path TO public;
CREATE EXTENSION IF NOT EXISTS plpgsql;
`);
}

/**
Expand Down Expand Up @@ -243,6 +246,9 @@ export class PGlite implements PGliteInterface {
async #runQuery<T>(query: string, params?: any[], options?: QueryOptions): Promise<Results<T>> {
return await this.#queryMutex.runExclusive(async () => {
// We need to parse, bind and execute a query with parameters
if (this.debug > 1) {
console.log("runQuery", query, params, options);
}
const parsedParams = params?.map((p) => serializeType(p)) || [];
let results;
try {
Expand Down Expand Up @@ -278,6 +284,9 @@ export class PGlite implements PGliteInterface {
async #runExec(query: string, options?: QueryOptions): Promise<Array<Results>> {
return await this.#queryMutex.runExclusive(async () => {
// No params so we can just send the query
if (this.debug > 1) {
console.log("runExec", query, options);
}
let results;
try {
results = await this.execProtocol(serialize.query(query));
Expand Down Expand Up @@ -390,7 +399,7 @@ export class PGlite implements PGliteInterface {
this.#parser = new Parser(); // Reset the parser
throw msg;
// TODO: Do we want to wrap the error in a custom error?
} else if (msg instanceof NoticeMessage) {
} else if (msg instanceof NoticeMessage && this.debug > 0) {
// Notice messages are warnings, we should log them
console.warn(msg);
}
Expand Down
163 changes: 149 additions & 14 deletions packages/pglite/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,27 @@ export const BOOL = 16,
REGNAMESPACE = 4089,
REGROLE = 4096;

export const arrayTypes = {
1001: BYTEA,
1002: CHAR,
1016: INT8,
1005: INT2,
1007: INT4,
1009: TEXT,
1028: OID,
199: JSON,
1021: FLOAT4,
1022: FLOAT8,
1015: VARCHAR,
3807: JSONB,
1182: DATE,
1115: TIMESTAMP,
1116: TIMESTAMPTZ,
};

export const types = {
string: {
to: TEXT,
to: 0,
from: [TEXT, VARCHAR],
serialize: (x: string) => x,
parse: (x: string) => x,
Expand All @@ -82,7 +100,14 @@ export const types = {
from: [INT8],
js: [BigInt],
serialize: (x: BigInt) => x.toString(),
parse: (x: string) => BigInt(x),
parse: (x: string) => {
const n = BigInt(x);
if (n < Number.MIN_SAFE_INTEGER || n > Number.MAX_SAFE_INTEGER) {
return n; // return BigInt
} else {
return Number(n); // in range of standard JS numbers so return number
}
},
},
json: {
to: JSON,
Expand Down Expand Up @@ -112,14 +137,26 @@ export const types = {
parse: (x: string): Uint8Array =>
new Uint8Array(Buffer.from(x.slice(2), "hex")),
},
array: {
to: 0,
from: Object.keys(arrayTypes).map((x) => +x),
serialize: (x: any[]) => serializeArray(x),
parse: (x: string, typeId?: number) => {
let parser;
if (typeId && typeId in arrayTypes) {
parser = parsers[arrayTypes[typeId as keyof typeof arrayTypes]];
}
return parseArray(x, parser);
},
},
} satisfies TypeHandlers;

export type TypeHandler = {
to: number;
from: number | number[];
js?: any;
serialize: (x: any) => string;
parse: (x: string) => any;
parse: (x: string, typeId?: number) => any;
};

export type TypeHandlers = {
Expand All @@ -132,28 +169,126 @@ export const parsers = defaultHandlers.parsers;
export const serializers = defaultHandlers.serializers;
export const serializerInstanceof = defaultHandlers.serializerInstanceof;

export function serializeType(x: any): [string, number] {
export type Serializer = (x: any) => [string, number];

export function serializerFor(x: any): Serializer {
if (Array.isArray(x)) {
return serializers.array;
}
const handler = serializers[typeof x];
if (handler) {
return handler(x);
} else {
for (const [Type, handler] of serializerInstanceof) {
if (x instanceof Type) {
return handler(x);
return handler;
}
for (const [Type, handler] of serializerInstanceof) {
if (x instanceof Type) {
return handler;
}
}
return serializers.json;
}

export function serializeType(x: any): [string | null, number] {
if (x === null) {
return [null, 0];
}
return serializerFor(x)(x);
}

function escapeElement(elementRepresentation: string) {
const escaped = elementRepresentation
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"');
return '"' + escaped + '"';
}

function serializeArray(x: any[]) {
let result = "{";
for (let i = 0; i < x.length; i++) {
if (i > 0) {
result = result + ",";
}
if (x[i] === null || typeof x[i] === "undefined") {
result = result + "NULL";
} else if (Array.isArray(x[i])) {
result = result + serializeArray(x[i]);
} else if (ArrayBuffer.isView(x[i])) {
let item = x[i];
if (!(item instanceof Buffer)) {
const buf = Buffer.from(item.buffer, item.byteOffset, item.byteLength);
if (buf.length === item.byteLength) {
item = buf;
} else {
item = buf.slice(item.byteOffset, item.byteOffset + item.byteLength);
}
}
result += "\\\\x" + item.toString("hex");
} else {
result += escapeElement(serializeType(x[i])[0]!);
}
return serializers.json(x);
}
result = result + "}";
return result;
}

export function parseArray(value: string, parser?: (s: string) => any) {
let i = 0;
let char = null;
let str = "";
let quoted = false;
let last = 0;
let p: string | undefined = undefined;

function loop(x: string): any[] {
const xs = [];
for (; i < x.length; i++) {
char = x[i];
if (quoted) {
if (char === "\\") {
str += x[++i];
} else if (char === '"') {
xs.push(parser ? parser(str) : str);
str = "";
quoted = x[i + 1] === '"';
last = i + 2;
} else {
str += char;
}
} else if (char === '"') {
quoted = true;
} else if (char === "{") {
last = ++i;
xs.push(loop(x));
} else if (char === "}") {
quoted = false;
last < i &&
xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i));
last = i + 1;
break;
} else if (char === "," && p !== "}" && p !== '"') {
xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i));
last = i + 1;
}
p = char;
}
last < i &&
xs.push(parser ? parser(x.slice(last, i + 1)) : x.slice(last, i + 1));
return xs;
}

return loop(value)[0];
}

export function parseType(
x: string,
type: number,
parsers?: ParserOptions
): any {
if (x === null) {
return null;
}
const handler = parsers?.[type] ?? defaultHandlers.parsers[type];
if (handler) {
return handler(x);
return handler(x, type);
} else {
return x;
}
Expand Down Expand Up @@ -182,11 +317,11 @@ function typeHandlers(types: TypeHandlers) {
return { parsers, serializers, serializerInstanceof };
},
{
parsers: {} as { [key: number | string]: (x: string) => any },
parsers: {} as { [key: number | string]: (x: string, typeId?: number) => any },
serializers: {} as {
[key: number | string]: (x: any) => [string, number];
[key: number | string]: Serializer;
},
serializerInstanceof: [] as Array<[any, (x: any) => [string, number]]>,
serializerInstanceof: [] as Array<[any, Serializer]>,
}
);
}
27 changes: 24 additions & 3 deletions packages/pglite/tests/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,17 @@ test("basic types", async (t) => {
date DATE,
timestamp TIMESTAMP,
json JSONB,
blob BYTEA
blob BYTEA,
array_text TEXT[],
array_number INT[],
nested_array_float FLOAT[][]
);
`);

await db.query(
`
INSERT INTO test (text, number, float, bigint, bool, date, timestamp, json, blob)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
INSERT INTO test (text, number, float, bigint, bool, date, timestamp, json, blob, array_text, array_number, nested_array_float)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);
`,
[
"test",
Expand All @@ -103,6 +106,9 @@ test("basic types", async (t) => {
new Date("2021-01-01T12:00:00"),
{ test: "test" },
Uint8Array.from([1, 2, 3]),
["test1", "test2", "test,3"],
[1, 2, 3],
[[1.1, 2.2], [3.3, 4.4]],
]
);

Expand All @@ -123,6 +129,9 @@ test("basic types", async (t) => {
timestamp: new Date("2021-01-01T12:00:00.000Z"),
json: { test: "test" },
blob: Uint8Array.from([1, 2, 3]),
array_text: ["test1", "test2", "test,3"],
array_number: [1, 2, 3],
nested_array_float: [[1.1, 2.2], [3.3, 4.4]],
},
],
fields: [
Expand Down Expand Up @@ -166,6 +175,18 @@ test("basic types", async (t) => {
name: "blob",
dataTypeID: 17,
},
{
name: "array_text",
dataTypeID: 1009,
},
{
name: "array_number",
dataTypeID: 1007,
},
{
name: "nested_array_float",
dataTypeID: 1022,
},
],
affectedRows: 0,
});
Expand Down
8 changes: 4 additions & 4 deletions packages/pglite/tests/types.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { types } from "../dist/index.js";

// Parse type tests

test("parse text 25", (t) => {
t.deepEqual(types.parseType("test", 25), "test");
test("parse text", (t) => {
t.deepEqual(types.parseType("test", 0), "test");
});

test("parse varchar 1043", (t) => {
Expand Down Expand Up @@ -32,7 +32,7 @@ test("parse float8 701", (t) => {
});

test("parse int8 20", (t) => {
t.deepEqual(types.parseType("1", 20), 1n);
t.deepEqual(types.parseType("1", 20), 1);
});

test("parse json 114", (t) => {
Expand Down Expand Up @@ -79,7 +79,7 @@ test("parse unknown", (t) => {
// Serialize type tests

test("serialize string", (t) => {
t.deepEqual(types.serializeType("test"), ["test", 25]);
t.deepEqual(types.serializeType("test"), ["test", 0]);
});

test("serialize number", (t) => {
Expand Down

0 comments on commit d056892

Please sign in to comment.