Skip to content

Commit da79aa2

Browse files
authored
Merge pull request #86 from malthe/name-transform
Column name transformation
2 parents 2548f87 + 1aea110 commit da79aa2

File tree

6 files changed

+110
-40
lines changed

6 files changed

+110
-40
lines changed

CHANGES.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
In the next release ...
2+
3+
- The query options now supports an optional `transform` parameter which takes
4+
a column name input, allowing the transformation of column names into for
5+
example camelcase.
6+
7+
- The `query` method now accepts a `QueryParameter` object as the
8+
first value, in addition to the query string, making it easier to
9+
make a query with additional configuration.
10+
111
1.6.0 (2023-12-13)
212
------------------
313

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,13 @@ The copy commands are not supported.
260260
pool.use(...)
261261
```
262262

263+
2. _How do I convert column names to camelcase?_ Use the `transform` option:
264+
265+
```typescript
266+
const camelcase = (s: string) => s.replace(/(_\w)/g, k => k[1].toUpperCase());
267+
const result = client.query({text: ..., transform: camelcase})
268+
```
269+
263270
## Benchmarking
264271

265272
Use the following environment variable to run tests in "benchmark" mode.

src/client.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as logger from './logging';
99

1010
import { postgresqlErrorCodes } from './errors';
1111
import { Queue } from './queue';
12-
import { Query } from './query';
12+
import { Query, QueryParameter } from './query';
1313

1414
import { ConnectionOptions, TLSSocket, connect as tls, createSecureContext } from 'tls';
1515

@@ -515,10 +515,11 @@ export class Client {
515515
}
516516

517517
prepare<T = ResultRecord>(
518-
text: string,
518+
text: QueryParameter | string,
519519
name?: string,
520520
types?: DataType[]): Promise<PreparedStatement<T>> {
521521

522+
const query = typeof text === 'string' ? {text} : text;
522523
const providedNameOrGenerated = name || (
523524
(this.config.preparedStatementPrefix ||
524525
defaults.preparedStatementPrefix) + (
@@ -529,7 +530,7 @@ export class Client {
529530
(resolve, reject) => {
530531
const errorHandler: ErrorHandler = (error) => reject(error);
531532
this.errorHandlerQueue.push(errorHandler);
532-
this.writer.parse(providedNameOrGenerated, text, types || []);
533+
this.writer.parse(providedNameOrGenerated, query.text, types || query.types || []);
533534
this.writer.describe(providedNameOrGenerated, 'S');
534535
this.preFlightQueue.push({
535536
descriptionHandler: (description: RowDescription) => {
@@ -557,7 +558,7 @@ export class Client {
557558
format?: DataFormat | DataFormat[],
558559
streams?: Record<string, Writable>,
559560
) => {
560-
const result = makeResult<T>();
561+
const result = makeResult<T>(query?.transform);
561562
result.nameHandler(description.names);
562563
const info = {
563564
handler: {
@@ -568,11 +569,11 @@ export class Client {
568569
};
569570
this.bindAndExecute(info, {
570571
name: providedNameOrGenerated,
571-
portal: portal || '',
572-
format: format || DataFormat.Binary,
572+
portal: portal || query.portal || '',
573+
format: format || query.format || DataFormat.Binary,
573574
values: values || [],
574575
close: false
575-
}, types);
576+
}, types || query.types);
576577

577578
return result.iterator
578579
}
@@ -588,23 +589,35 @@ export class Client {
588589
});
589590
}
590591

592+
/**
593+
* Send a query to the database.
594+
*
595+
* The query string is given as the first argument, or pass a {@link QueryParameter}
596+
* object which provides more control.
597+
*
598+
* @param text - The query string, or pass a {@link QueryParameter}
599+
* object which provides more control (including streaming values into a socket).
600+
* @param values - The query parameters, corresponding to $1, $2, etc.
601+
* @param types - Allows making the database native type explicit for some or all
602+
* columns.
603+
* @param format - Whether column data should be transferred using text or binary mode.
604+
* @param streams - A mapping from column name to a socket, e.g. an open file.
605+
* @returns A promise for the query results.
606+
*/
591607
query<T = ResultRecord>(
592-
text: string,
608+
text: QueryParameter | string,
593609
values?: any[],
594610
types?: DataType[],
595611
format?: DataFormat | DataFormat[],
596612
streams?: Record<string, Writable>):
597613
ResultIterator<T> {
598-
const query =
599-
(typeof text === 'string') ?
600-
new Query(
601-
text,
602-
values, {
603-
types: types,
604-
format: format,
605-
streams: streams,
606-
}) :
607-
text;
614+
const query = new Query(
615+
text,
616+
values, {
617+
types: types,
618+
format: format,
619+
streams: streams,
620+
});
608621
return this.execute<T>(query);
609622
}
610623

@@ -651,11 +664,11 @@ export class Client {
651664
const text = query.text;
652665
const values = query.values || [];
653666
const options = query.options;
654-
const format = options ? options.format : undefined;
655-
const types = options ? options.types : undefined;
656-
const streams = options ? options.streams : undefined;
657-
const portal = (options ? options.portal : undefined) || '';
658-
const result = makeResult<T>();
667+
const format = options?.format;
668+
const types = options?.types;
669+
const streams = options?.streams;
670+
const portal = options?.portal || '';
671+
const result = makeResult<T>(options?.transform);
659672

660673
const descriptionHandler = (description: RowDescription) => {
661674
result.nameHandler(description.names);
@@ -667,7 +680,7 @@ export class Client {
667680
};
668681

669682
if (values && values.length) {
670-
const name = (options ? options.name : undefined) || (
683+
const name = (options?.name) || (
671684
(this.config.preparedStatementPrefix ||
672685
defaults.preparedStatementPrefix) + (
673686
this.nextPreparedStatementId++

src/query.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,46 @@ import {
55
} from './types';
66

77
export interface QueryOptions {
8+
/** The query name. */
89
readonly name: string;
10+
/** Whether to use the default portal (i.e. unnamed) or provide a name. */
911
readonly portal: string;
12+
/** Allows making the database native type explicit for some or all columns. */
1013
readonly types: DataType[];
14+
/** Whether column data should be transferred using text or binary mode. */
1115
readonly format: DataFormat | DataFormat[];
16+
/** A mapping from column name to a socket, e.g. an open file. */
1217
readonly streams: Record<string, Writable>;
18+
/** Allows the transformation of column names as returned by the database. */
19+
readonly transform: (name: string) => string;
1320
}
1421

22+
/**
23+
* A query parameter can be used in place of a query text as the first argument
24+
* to the {@link Client.query} method.
25+
* @interface
26+
*/
27+
export type QueryParameter = Partial<QueryOptions> & { text: string };
28+
29+
/**
30+
* A complete query object, ready to send to the database.
31+
*/
1532
export class Query {
33+
public readonly text: string;
34+
public readonly values?: any[];
35+
public readonly options?: Partial<QueryOptions>;
36+
1637
constructor(
17-
public readonly text: string,
18-
public readonly values?: any[],
19-
public readonly options?: Partial<QueryOptions>
20-
) { }
38+
text: QueryParameter | string,
39+
values?: any[],
40+
options?: Partial<QueryOptions>
41+
) {
42+
this.values = values;
43+
this.options = options;
44+
if (typeof text === 'string') {
45+
this.text = text;
46+
} else {
47+
({ text: this.text, ...this.options } = {...this.options, ...text});
48+
}
49+
}
2150
}

src/result.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type Callback<T> = (item: T) => void;
77
type ResultHandler = (resolve: Callback<Resolution>, reject: Callback<Error | DatabaseError>) => void;
88

99
/** The default result type, used if no generic type parameter is specified. */
10-
export type ResultRecord = Record<string, any>
10+
export type ResultRecord<T = any> = Record<string, T>;
1111

1212

1313
function makeRecord<T>(names: string[], data: ReadonlyArray<any>): T {
@@ -209,7 +209,7 @@ export type NameHandler = Callback<string[]>;
209209

210210
ResultIterator.prototype.constructor = Promise
211211

212-
export function makeResult<T>() {
212+
export function makeResult<T>(transform?: (name: string) => string) {
213213
let dataHandler: DataHandler | null = null;
214214

215215
const names: string[] = [];
@@ -232,6 +232,9 @@ export function makeResult<T>() {
232232

233233
const nameHandler = (ns: string[]) => {
234234
names.length = 0;
235+
if (transform) {
236+
ns = ns.map(transform);
237+
}
235238
names.push(...ns);
236239
}
237240

test/client.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -239,16 +239,11 @@ describe('Query', () => {
239239
expect(result.rows.length).toEqual(1);
240240
});
241241

242-
testWithClient('Prepared statement', async (client) => {
243-
const count = 5;
244-
expect.assertions(count * 2);
245-
await client.query('prepare test (int) as select $1');
246-
for (let i = 0; i < count; i++) {
247-
const result = await client.query('execute test(1)');
248-
const rows = result.rows;
249-
expect(rows.length).toEqual(1);
250-
expect(rows[0]).toEqual([1]);
251-
}
242+
testWithClient('Name transform', async (client) => {
243+
expect.assertions(1);
244+
const query = {text: 'select 1 as foo', transform: (s: string) => s.toUpperCase()};
245+
const result = await client.query(query);
246+
expect(result.names).toEqual(['FOO']);
252247
});
253248

254249
testWithClient('Listen/notify', async (client) => {
@@ -487,6 +482,19 @@ describe('Query', () => {
487482
await stmt.close();
488483
});
489484

485+
testWithClient(
486+
'Prepare and execute (SELECT)',
487+
async (client) => {
488+
const query = {text: 'select $1::int as i', transform: (s: string) => s.toUpperCase()};
489+
const stmt = await client.prepare(query);
490+
await expect(stmt.execute([1])).resolves.toEqual(
491+
{ names: ['I'], rows: [[1]], status: 'SELECT 1' }
492+
);
493+
const result = await stmt.execute([2]);
494+
expect(result.rows).toEqual([[2]]);
495+
await stmt.close();
496+
});
497+
490498
testWithClient(
491499
'Prepare and execute (INSERT)',
492500
async (client) => {

0 commit comments

Comments
 (0)