Skip to content

Commit

Permalink
Add decode strategy control (#456)
Browse files Browse the repository at this point in the history
* feate: add encoding strategy control

* chore: add encoding strategy tests

* chore: fix file formatting

* chore: fix lint issue of unused import

* chore: fix variable anem to make camelcase
  • Loading branch information
bombillazo authored Feb 12, 2024
1 parent c5f5b2d commit 2785855
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 144 deletions.
4 changes: 2 additions & 2 deletions connection/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ export class Connection {
case INCOMING_QUERY_MESSAGES.DATA_ROW: {
const row_data = parseRowDataMessage(current_message);
try {
result.insertRow(row_data);
result.insertRow(row_data, this.#connection_params.controls);
} catch (e) {
error = e;
}
Expand Down Expand Up @@ -862,7 +862,7 @@ export class Connection {
case INCOMING_QUERY_MESSAGES.DATA_ROW: {
const row_data = parseRowDataMessage(current_message);
try {
result.insertRow(row_data);
result.insertRow(row_data, this.#connection_params.controls);
} catch (e) {
error = e;
}
Expand Down
39 changes: 34 additions & 5 deletions connection/connection_params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,43 @@ export interface TLSOptions {
caCertificates: string[];
}

/**
* Control the behavior for the client instance
*/
export type ClientControls = {
/**
* The strategy to use when decoding binary fields
*
* `string` : all values are returned as string, and the user has to take care of parsing
* `auto` : deno-postgres parses the data into JS objects (as many as possible implemented, non-implemented parsers would still return strings)
*
* Default: `auto`
*
* Future strategies might include:
* - `strict` : deno-postgres parses the data into JS objects, and if a parser is not implemented, it throws an error
* - `raw` : the data is returned as Uint8Array
*/
decodeStrategy?: "string" | "auto";
};

/** The Client database connection options */
export type ClientOptions = {
/** Name of the application connecing to the database */
applicationName?: string;
/** Additional connection options */
connection?: Partial<ConnectionOptions>;
/** Control the client behavior */
controls?: ClientControls;
/** The database name */
database?: string;
/** The name of the host */
hostname?: string;
/** The type of host connection */
host_type?: "tcp" | "socket";
/** Additional client options */
/**
* Additional connection URI options
* https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
*/
options?: string | Record<string, string>;
/** The database user password */
password?: string;
Expand All @@ -118,14 +142,18 @@ export type ClientOptions = {
/** The configuration options required to set up a Client instance */
export type ClientConfiguration =
& Required<
Omit<ClientOptions, "password" | "port" | "tls" | "connection" | "options">
Omit<
ClientOptions,
"password" | "port" | "tls" | "connection" | "options" | "controls"
>
>
& {
connection: ConnectionOptions;
controls?: ClientControls;
options: Record<string, string>;
password?: string;
port: number;
tls: TLSOptions;
connection: ConnectionOptions;
options: Record<string, string>;
};

function formatMissingParams(missingParams: string[]) {
Expand Down Expand Up @@ -168,7 +196,7 @@ function assertRequiredOptions(

// TODO
// Support more options from the spec
/** options from URI per https://www.postgresql.org/docs/14/libpq-connect.html#LIBPQ-CONNSTRING */
/** options from URI per https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING */
interface PostgresUri {
application_name?: string;
dbname?: string;
Expand Down Expand Up @@ -447,6 +475,7 @@ export function createParams(
caCertificates: params?.tls?.caCertificates ?? [],
},
user: params.user ?? pgEnv.user,
controls: params.controls,
};

assertRequiredOptions(
Expand Down
14 changes: 12 additions & 2 deletions query/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
decodeTid,
decodeTidArray,
} from "./decoders.ts";
import { ClientControls } from "../connection/connection_params.ts";

export class Column {
constructor(
Expand All @@ -58,7 +59,7 @@ const decoder = new TextDecoder();
// TODO
// Decode binary fields
function decodeBinary() {
throw new Error("Not implemented!");
throw new Error("Decoding binary data is not implemented!");
}

function decodeText(value: Uint8Array, typeOid: number) {
Expand Down Expand Up @@ -208,10 +209,19 @@ function decodeText(value: Uint8Array, typeOid: number) {
}
}

export function decode(value: Uint8Array, column: Column) {
export function decode(
value: Uint8Array,
column: Column,
controls?: ClientControls,
) {
if (column.format === Format.BINARY) {
return decodeBinary();
} else if (column.format === Format.TEXT) {
// If the user has specified a decode strategy, use that
if (controls?.decodeStrategy === "string") {
return decoder.decode(value);
}
// default to 'auto' mode, which uses the typeOid to determine the decoding strategy
return decodeText(value, column.typeOid);
} else {
throw new Error(`Unknown column format: ${column.format}`);
Expand Down
9 changes: 5 additions & 4 deletions query/query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { encodeArgument, type EncodedArg } from "./encode.ts";
import { type Column, decode } from "./decode.ts";
import { type Notice } from "../connection/message.ts";
import { type ClientControls } from "../connection/connection_params.ts";

// TODO
// Limit the type of parameters that can be passed
Expand Down Expand Up @@ -242,7 +243,7 @@ export class QueryArrayResult<
/**
* Insert a row into the result
*/
insertRow(row_data: Uint8Array[]) {
insertRow(row_data: Uint8Array[], controls?: ClientControls) {
if (!this.rowDescription) {
throw new Error(
"The row descriptions required to parse the result data weren't initialized",
Expand All @@ -256,7 +257,7 @@ export class QueryArrayResult<
if (raw_value === null) {
return null;
}
return decode(raw_value, column);
return decode(raw_value, column, controls);
}) as T;

this.rows.push(row);
Expand Down Expand Up @@ -303,7 +304,7 @@ export class QueryObjectResult<
/**
* Insert a row into the result
*/
insertRow(row_data: Uint8Array[]) {
insertRow(row_data: Uint8Array[], controls?: ClientControls) {
if (!this.rowDescription) {
throw new Error(
"The row description required to parse the result data wasn't initialized",
Expand Down Expand Up @@ -364,7 +365,7 @@ export class QueryObjectResult<
if (raw_value === null) {
row[columns[index]] = null;
} else {
row[columns[index]] = decode(raw_value, current_column);
row[columns[index]] = decode(raw_value, current_column, controls);
}

return row;
Expand Down
16 changes: 11 additions & 5 deletions tests/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { ClientConfiguration } from "../connection/connection_params.ts";
import {
ClientConfiguration,
ClientOptions,
} from "../connection/connection_params.ts";
import config_file1 from "./config.json" with { type: "json" };

type TcpConfiguration = Omit<ClientConfiguration, "connection"> & {
Expand Down Expand Up @@ -67,17 +70,20 @@ export const getClearSocketConfiguration = (): SocketConfiguration => {
};

/** MD5 authenticated user with privileged access to the database */
export const getMainConfiguration = (): TcpConfiguration => {
export const getMainConfiguration = (
_config?: ClientOptions,
): TcpConfiguration => {
return {
applicationName: config.postgres_md5.applicationName,
database: config.postgres_md5.database,
hostname: config.postgres_md5.hostname,
host_type: "tcp",
options: {},
password: config.postgres_md5.password,
user: config.postgres_md5.users.main,
..._config,
options: {},
port: config.postgres_md5.port,
tls: enabled_tls,
user: config.postgres_md5.users.main,
host_type: "tcp",
};
};

Expand Down
77 changes: 77 additions & 0 deletions tests/decode_test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Column, decode } from "../query/decode.ts";
import {
decodeBigint,
decodeBigintArray,
Expand All @@ -17,6 +18,7 @@ import {
decodeTid,
} from "../query/decoders.ts";
import { assertEquals, assertThrows } from "./test_deps.ts";
import { Oid } from "../query/oid.ts";

Deno.test("decodeBigint", function () {
assertEquals(decodeBigint("18014398509481984"), 18014398509481984n);
Expand Down Expand Up @@ -248,3 +250,78 @@ Deno.test("decodeTid", function () {
29383838509481984n,
]);
});

Deno.test("decode strategy", function () {
const testValues = [
{
value: "40",
column: new Column("test", 0, 0, Oid.int4, 0, 0, 0),
parsed: 40,
},
{
value: "my_value",
column: new Column("test", 0, 0, Oid.text, 0, 0, 0),
parsed: "my_value",
},
{
value: "[(100,50),(350,350)]",
column: new Column("test", 0, 0, Oid.path, 0, 0, 0),
parsed: [
{ x: "100", y: "50" },
{ x: "350", y: "350" },
],
},
{
value: '{"value_1","value_2","value_3"}',
column: new Column("test", 0, 0, Oid.text_array, 0, 0, 0),
parsed: ["value_1", "value_2", "value_3"],
},
{
value: "1997-12-17 07:37:16-08",
column: new Column("test", 0, 0, Oid.timestamp, 0, 0, 0),
parsed: new Date("1997-12-17 07:37:16-08"),
},
{
value: "Yes",
column: new Column("test", 0, 0, Oid.bool, 0, 0, 0),
parsed: true,
},
{
value: "<(12.4,2),3.5>",
column: new Column("test", 0, 0, Oid.circle, 0, 0, 0),
parsed: { point: { x: "12.4", y: "2" }, radius: "3.5" },
},
{
value: '{"test":1,"val":"foo","example":[1,2,false]}',
column: new Column("test", 0, 0, Oid.jsonb, 0, 0, 0),
parsed: { test: 1, val: "foo", example: [1, 2, false] },
},
{
value: "18014398509481984",
column: new Column("test", 0, 0, Oid.int8, 0, 0, 0),
parsed: 18014398509481984n,
},
{
value: "{3.14,1.11,0.43,200}",
column: new Column("test", 0, 0, Oid.float4_array, 0, 0, 0),
parsed: [3.14, 1.11, 0.43, 200],
},
];

for (const testValue of testValues) {
const encodedValue = new TextEncoder().encode(testValue.value);

// check default behavior
assertEquals(decode(encodedValue, testValue.column), testValue.parsed);
// check 'auto' behavior
assertEquals(
decode(encodedValue, testValue.column, { decodeStrategy: "auto" }),
testValue.parsed,
);
// check 'string' behavior
assertEquals(
decode(encodedValue, testValue.column, { decodeStrategy: "string" }),
testValue.value,
);
}
});
Loading

1 comment on commit 2785855

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No typecheck tests failure

This error was most likely caused by incorrect type stripping from the SWC crate

Please report the following failure to https://github.com/denoland/deno with a reproduction of the current commit

Failure log

Please sign in to comment.