Skip to content

Query result (and related objects) is now generic on the row type #82

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 1 commit into from
Dec 6, 2023
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
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/no-explicit-any": 0
}
}
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
In next release ...

- The `Value` type has been replaced with `any`, motivated by the new
generic result type as well as the possibility to implement custom
value readers which could return objects of any type.

- Query results are now generic with typing support for the `get`
method on each row.

In addition, a new `map` method now produces records which conform
to the specified type. This method is available on all of the result
objects.

- Use lookup table to optimize `get` method.

- The `connect` method now returns a boolean status of whether the
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ The client uses an async/await-based programming model.
```typescript
import { Client } from 'ts-postgres';

interface Greeting {
message: string;
}

async function main() {
const client = new Client();
await client.connect();

try {
// Querying the client returns a query result promise
// which is also an asynchronous result iterator.
const result = client.query(
// The query method is generic on the result row.
const result = client.query<Greeting>(
"SELECT 'Hello ' || $1 || '!' AS message",
['world']
);
Expand Down Expand Up @@ -112,9 +115,14 @@ const query = new Query(
"SELECT 'Hello ' || $1 || '!' AS message",
['world']
);
const result = await client.execute(query);
const result = await client.execute<Greeting>(query);
```

If the row type is omitted, it defaults to `Record<string, any>`, but
providing a type ensures that the row values are typed, both when
accessed via the `get` method or if the row is mapped to a record
using the `map` method.

### Passing query parameters

Query parameters use the format `$1`, `$2` etc.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 21 additions & 30 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import { ConnectionOptions, TLSSocket, connect as tls, createSecureContext } fro

import {
DataHandler,
Result as _Result,
ResultIterator as _ResultIterator,
ResultRow as _ResultRow,
ResultIterator,
ResultRecord,
makeResult
} from './result';

Expand All @@ -37,19 +36,11 @@ import {
import {
DataFormat,
DataType,
Row,
Value,
ValueTypeReader
} from './types';

import { md5 } from './utils';

export type Result = _Result<Value>;

export type ResultIterator = _ResultIterator<Value>;

export type ResultRow = _ResultRow<Value>;

export type Connect = Error | null;

export type End = void;
Expand All @@ -67,7 +58,7 @@ export interface ClientNotice extends DatabaseError {

export interface DataTypeError {
dataType: DataType,
value: Value
value: any
}

export enum SSLMode {
Expand Down Expand Up @@ -100,14 +91,14 @@ export interface Notification {
payload?: string
}

export interface PreparedStatement {
export interface PreparedStatement<T = ResultRecord> {
close: (portal?: string) => Promise<void>;
execute: (
values?: Value[],
values?: any[],
portal?: string,
format?: DataFormat | DataFormat[],
streams?: Record<string, Writable>,
) => ResultIterator
) => ResultIterator<T>
}

type Callback<T> = (data: T) => void;
Expand All @@ -127,7 +118,7 @@ type Event = (
type CloseHandler = () => void;

interface RowDataHandler {
callback: DataHandler<Row>,
callback: DataHandler<any[]>,
streams: Record<string, Writable>,
}

Expand All @@ -154,7 +145,7 @@ interface Bind {
name: string;
format: DataFormat | DataFormat[]
portal: string;
values: Value[],
values: any[],
close: boolean
}

Expand Down Expand Up @@ -188,7 +179,7 @@ export class Client {
private expect = 5;
private stream = new Socket();
private mustDrain = false;
private activeRow: Array<Value> | null = null;
private activeRow: Array<any> | null = null;

private bindQueue = new Queue<RowDataHandlerInfo | null>();
private closeHandlerQueue = new Queue<CloseHandler | null>();
Expand Down Expand Up @@ -513,18 +504,18 @@ export class Client {
}
}

prepare(
prepare<T = ResultRecord>(
text: string,
name?: string,
types?: DataType[]): Promise<PreparedStatement> {
types?: DataType[]): Promise<PreparedStatement<T>> {

const providedNameOrGenerated = name || (
(this.config.preparedStatementPrefix ||
defaults.preparedStatementPrefix) + (
this.nextPreparedStatementId++
));

return new Promise<PreparedStatement>(
return new Promise<PreparedStatement<T>>(
(resolve, reject) => {
const errorHandler: ErrorHandler = (error) => reject(error);
this.errorHandlerQueue.push(errorHandler);
Expand All @@ -551,12 +542,12 @@ export class Client {
);
},
execute: (
values?: Value[],
values?: any[],
portal?: string,
format?: DataFormat | DataFormat[],
streams?: Record<string, Writable>,
) => {
const result = makeResult<Value>();
const result = makeResult<T>();
result.nameHandler(description.names);
const info = {
handler: {
Expand Down Expand Up @@ -587,13 +578,13 @@ export class Client {
});
}

query(
query<T = ResultRecord>(
text: string,
values?: Value[],
values?: any[],
types?: DataType[],
format?: DataFormat | DataFormat[],
streams?: Record<string, Writable>):
ResultIterator {
ResultIterator<T> {
const query =
(typeof text === 'string') ?
new Query(
Expand All @@ -604,7 +595,7 @@ export class Client {
streams: streams,
}) :
text;
return this.execute(query);
return this.execute<T>(query);
}

private bindAndExecute(
Expand Down Expand Up @@ -642,7 +633,7 @@ export class Client {
this.send();
}

execute(query: Query): ResultIterator {
execute<T = ResultRecord>(query: Query): ResultIterator<T> {
if (this.closed && !this.connecting) {
throw new Error('Connection is closed.');
}
Expand All @@ -654,7 +645,7 @@ export class Client {
const types = options ? options.types : undefined;
const streams = options ? options.streams : undefined;
const portal = (options ? options.portal : undefined) || '';
const result = makeResult<Value>();
const result = makeResult<T>();

const descriptionHandler = (description: RowDescription) => {
result.nameHandler(description.names);
Expand Down Expand Up @@ -877,7 +868,7 @@ export class Client {

if (row === null) {
const count = buffer.readInt16BE(start);
row = new Array<Value>(count);
row = new Array<any>(count);
}

const startRowData = start + 2;
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './client';
export * from './types';
export * from './query';
export * from './result';
export { DatabaseError } from './protocol';
25 changes: 11 additions & 14 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import { sum } from './utils';
import {
arrayDataTypeMapping,
isPoint,
ArrayValue,
DataFormat,
DataType,
Primitive,
Value,
ValueTypeReader
} from './types';

Expand Down Expand Up @@ -278,7 +275,7 @@ export function readRowDescription(

export function readRowData(
buffer: Buffer,
row: Array<Value>,
row: Array<any>,
columnSpecification: Uint32Array,
encoding: BufferEncoding,
types: ReadonlyMap<DataType, ValueTypeReader> | null,
Expand Down Expand Up @@ -309,7 +306,7 @@ export function readRowData(
const remaining = end - bufferLength;
const partial = remaining > 0;

let value: Value = null;
let value: any = null;

if (start < end) {
const spec = columnSpecification[j];
Expand Down Expand Up @@ -449,7 +446,7 @@ export function readRowData(
let offset = start;

const readArray = (size: number) => {
const array: ArrayValue<Primitive> =
const array: any[] =
new Array(size);

for (let j = 0; j < size; j++) {
Expand All @@ -476,7 +473,7 @@ export function readRowData(
offset += 8;
value = readArray(size);
} else {
const arrays: ArrayValue<Primitive>[] =
const arrays: any[][] =
new Array(dimCount);
const dims = new Uint32Array(dimCount);

Expand Down Expand Up @@ -624,7 +621,7 @@ export class Reader {
}

readRowData(
row: Array<Value>,
row: any[],
columnSpecification: Uint32Array,
encoding: BufferEncoding,
types: ReadonlyMap<DataType, ValueTypeReader> | null,
Expand Down Expand Up @@ -654,7 +651,7 @@ export class Writer {
name: string,
portal: string,
format: DataFormat | DataFormat[] = DataFormat.Binary,
values: Value[] = [],
values: any[] = [],
types: DataType[] = []) {
// We silently ignore any mismatch here, assuming that the
// query will fail and make the error evident.
Expand Down Expand Up @@ -690,7 +687,7 @@ export class Writer {
}
};

const addBinaryValue = (value: Value, dataType: DataType): number => {
const addBinaryValue = (value: any, dataType: DataType): number => {
let size = -1;
const setSize = reserve(SegmentType.Int32BE);

Expand Down Expand Up @@ -836,7 +833,7 @@ export class Writer {
};

const addBinaryArray = (
value: Value[],
value: any[],
dataType: DataType): number => {
const setDimCount = reserve(SegmentType.Int32BE);
add(SegmentType.Int32BE, 1);
Expand All @@ -845,7 +842,7 @@ export class Writer {
let bytes = 12;
let dimCount = 0;

const go = (level: number, value: Value[]) => {
const go = (level: number, value: any[]) => {
const length = value.length;
if (length === 0) return;

Expand Down Expand Up @@ -874,7 +871,7 @@ export class Writer {
}

const getTextFromValue = (
value: Value,
value: any,
dataType: DataType): null | string | string[] => {
if (value === null) return null;

Expand Down Expand Up @@ -929,7 +926,7 @@ export class Writer {
}

const getTextFromArray = (
value: Value[],
value: any[],
dataType: DataType): string[] => {
const strings: string[] = [];
strings.push('{');
Expand Down
3 changes: 1 addition & 2 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Writable } from 'stream';
import {
DataFormat,
DataType,
Value
} from './types';

export interface QueryOptions {
Expand All @@ -16,7 +15,7 @@ export interface QueryOptions {
export class Query {
constructor(
public readonly text: string,
public readonly values?: Value[],
public readonly values?: any[],
public readonly options?: Partial<QueryOptions>
) { }
}
Loading