Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add decode strategy control #456

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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