Skip to content

SQL NULL mapping #79

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

Closed
wants to merge 2 commits into from
Closed
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
12 changes: 11 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
In next release ...

- Database name now implicitly defaults to the user name.
## New features

- Add additional client connection configuration options.

- Result rows are now themselves iterable.


## Changes

- Database name now implicitly defaults to the user name.

- The SQL NULL value is now mapped 1:1 with `undefined` rather than
`null`. This change is motivated by TypeScript's more natural use of
undefined values since it denotes an optional or missing property.

- Use `bigint` everywhere as a type instead of `BigInt`.


1.4.0 (2023-11-10)
------------------

Expand Down
6 changes: 3 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ export class Client {
}

private receive(buffer: Buffer, offset: number, size: number): number {
const types = this.config.types || null;
const types = this.config.types;
let read = 0;

while (size >= this.expect + read) {
Expand Down Expand Up @@ -869,8 +869,8 @@ export class Client {

const hasStreams = Object.keys(streams).length > 0;
const mappedStreams = hasStreams ? names.map(
name => streams[name] || null
) : null;
name => streams[name]
) : undefined;

while (true) {
mtype = buffer.readInt8(frame);
Expand Down
38 changes: 18 additions & 20 deletions src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export function readRowDescription(
const dataType = buffer.readInt32BE(j + 7);
const innerDataType = arrayDataTypeMapping.get(dataType);
const isArray = (typeof innerDataType !== 'undefined');
const typeReader = (types) ? types.get(dataType) : undefined;
const typeReader = types ? types.get(dataType) : undefined;

columns[i] =
((innerDataType || dataType))
Expand All @@ -281,8 +281,8 @@ export function readRowData(
row: Array<Value>,
columnSpecification: Uint32Array,
encoding: BufferEncoding,
types: ReadonlyMap<DataType, ValueTypeReader> | null,
streams: ReadonlyArray<Writable | null> | null,
types?: ReadonlyMap<DataType, ValueTypeReader>,
streams?: ReadonlyArray<Writable | undefined>,
): number {
const columns = row.length;
const bufferLength = buffer.length;
Expand All @@ -309,15 +309,15 @@ export function readRowData(
const remaining = end - bufferLength;
const partial = remaining > 0;

let value: Value = null;
let value: Value = undefined;

if (start < end) {
const spec = columnSpecification[j];
let skip = false;

if (streams !== null && spec === DataType.Bytea) {
if (streams !== undefined && spec === DataType.Bytea) {
const stream = streams[j];
if (stream !== null) {
if (stream !== undefined) {
const slice = buffer.slice(start, end);
const alloc = Buffer.allocUnsafe(slice.length);
slice.copy(alloc, 0, 0, slice.length);
Expand Down Expand Up @@ -346,7 +346,7 @@ export function readRowData(
const isReader = (spec & readerMask) !== 0;

if (isReader) {
const reader = (types) ? types.get(dataType) : null;
const reader = types ? types.get(dataType) : undefined;
if (reader) {
value = reader(
buffer,
Expand All @@ -358,14 +358,14 @@ export function readRowData(
}
} else {
const read = (t: DataType, start: number, end: number) => {
if (start === end) return null;
if (start === end) return undefined;

/* Cutoff for system object OIDs;
see comments in src/include/access/transam.h

We do not support user object OIDs.
*/
if (t >= DataType.MinUserOid) return null;
if (t >= DataType.MinUserOid) return undefined;

switch (t) {
case DataType.Bool:
Expand Down Expand Up @@ -442,7 +442,7 @@ export function readRowData(
case DataType.Uuid:
return formatUuid(buffer.slice(start, end));
}
return null;
return;
};

if (isArray) {
Expand All @@ -455,7 +455,7 @@ export function readRowData(
for (let j = 0; j < size; j++) {
const length = buffer.readInt32BE(offset);
offset += 4;
let value = null;
let value = undefined;
if (length >= 0) {
const elementStart = offset;
offset = elementStart + length;
Expand Down Expand Up @@ -627,8 +627,8 @@ export class Reader {
row: Array<Value>,
columnSpecification: Uint32Array,
encoding: BufferEncoding,
types: ReadonlyMap<DataType, ValueTypeReader> | null,
streams: ReadonlyArray<Writable | null> | null,
types?: ReadonlyMap<DataType, ValueTypeReader>,
streams?: ReadonlyArray<Writable | undefined>,
) {
return readRowData(
this.buffer.slice(this.start, this.end),
Expand Down Expand Up @@ -694,7 +694,7 @@ export class Writer {
let size = -1;
const setSize = reserve(SegmentType.Int32BE);

if (value === null) {
if (value === undefined) {
setSize(-1);
return 0;
}
Expand Down Expand Up @@ -875,8 +875,8 @@ export class Writer {

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

switch (dataType) {
case DataType.Bool:
Expand Down Expand Up @@ -924,8 +924,6 @@ export class Writer {
throw new Error(`Unsupported data type: ${dataType}`);
}
}

return null;
}

const getTextFromArray = (
Expand All @@ -949,7 +947,7 @@ export class Writer {
if (result instanceof Array) {
strings.push(...result);
} else {
strings.push((result === null) ? 'null' : escape(result));
strings.push((result === undefined) ? 'null' : escape(result));
}
}
strings.push('}');
Expand All @@ -972,7 +970,7 @@ export class Writer {
add(SegmentType.Buffer,
makeBuffer(s, this.encoding)))) :
add(SegmentType.Buffer,
(result === null) ?
(result === undefined) ?
nullBuffer :
makeBuffer(result, this.encoding)
);
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ export type Builtin =
bigint |
boolean |
number |
null |
string;
string |
undefined;

export type AnyJson = boolean | number | string | null | JsonArray | JsonMap;

Expand Down
28 changes: 14 additions & 14 deletions test/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,28 @@ describe('Result', () => {
expect(row.get('bad')).toEqual(undefined);
});

testWithClient('Parse array containing null', async (client) => {
testWithClient('Parse array containing NULL', async (client) => {
expect.assertions(1);
const row = await client.query(
'select ARRAY[null::text] as a'
).one();
expect(row.get('a')).toEqual([null]);
expect(row.get('a')).toEqual([undefined]);
});

testWithClient('Format array containing null value', async (client) => {
testWithClient('Format array containing undefined value', async (client) => {
expect.assertions(1);
const row = await client.query(
'select $1::text[] as a', [[null]]
'select $1::text[] as a', [[undefined]]
).one();
expect(row.get('a')).toEqual([null]);
expect(row.get('a')).toEqual([undefined]);
});

testWithClient('Format null-array', async (client) => {
testWithClient('Format undefined array', async (client) => {
expect.assertions(1);
const row = await client.query(
'select $1::text[] as a', [null]
'select $1::text[] as a', [undefined]
).one();
expect(row.get('a')).toEqual(null);
expect(row.get('a')).toEqual(undefined);
});

testWithClient('One', async (client) => {
Expand Down Expand Up @@ -127,15 +127,15 @@ describe('Result', () => {
})
});

testWithClient('Multiple null params', async (client) => {
testWithClient('Multiple undefined params', async (client) => {
expect.assertions(3);
const row = await client.query(
'select $1::text as a, $2::text[] as b, $3::jsonb[] as c',
[null, null, null]
[undefined, undefined, undefined]
).one();
expect(row.get('a')).toBeNull()
expect(row.get('b')).toBeNull();
expect(row.get('c')).toBeNull();
expect(row.get('a')).toBeUndefined()
expect(row.get('b')).toBeUndefined();
expect(row.get('c')).toBeUndefined();
});

testWithClient('Synchronous iteration', async (client) => {
Expand Down Expand Up @@ -167,6 +167,6 @@ describe('Result', () => {
testWithClient('Null typed array', async (client) => {
expect.assertions(1);
const row = await client.query('select null::text[] as value').one();
expect(row.get('value')).toEqual(null);
expect(row.get('value')).toEqual(undefined);
});
});
56 changes: 28 additions & 28 deletions test/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ function testType<T extends Value>(
const testParam = (format: DataFormat) => {
testWithClient('Param', async (client) => {
expect.assertions(3);
const query = expected !== null
const query = expected !== undefined
? getComparisonQueryFor(dataType, expression)
: 'select $1 is null';
await client.query(
(expected !== null) ? query + ' where $1 is not null' : query,
(expected !== undefined) ? query + ' where $1 is not null' : query,
[expected], [dataType], format)
.then(
(result) => {
Expand Down Expand Up @@ -165,14 +165,14 @@ describe('Types', () => {
DataType.Date,
'\'0002-12-31 BC\'::date',
utc_date(-1, 11, 31));
testType<(Date | null)[]>(
testType<(Date | undefined)[]>(
DataType.ArrayTimestamptz,
'ARRAY[null,\'1999-12-31 23:59:59Z\']::timestamptz[]',
[null, utc_date(1999, 11, 31, 23, 59, 59)]);
testType<(Date | null)[][]>(
[undefined, utc_date(1999, 11, 31, 23, 59, 59)]);
testType<(Date | undefined)[][]>(
DataType.ArrayTimestamptz,
'ARRAY[ARRAY[null],ARRAY[\'1999-12-31 23:59:59Z\']]::timestamptz[][]',
[[null], [utc_date(1999, 11, 31, 23, 59, 59)]]);
[[undefined], [utc_date(1999, 11, 31, 23, 59, 59)]]);
testType<Point>(
DataType.Point,
'\'(1,2)\'::Point',
Expand Down Expand Up @@ -232,20 +232,20 @@ describe('Types', () => {
DataType.ArrayText, '\'{a}\'::text[]', ['a']);
testType<string[]>(
DataType.ArrayText, '\'{"a,"}\'::text[]', ['a,']);
testType<(string | null)[]>(
testType<(string | undefined)[]>(
DataType.ArrayText,
'ARRAY[null]::text[]',
[null]
[undefined]
);
testType<(string | null)[]>(
testType<(string | undefined)[]>(
DataType.ArrayText,
`ARRAY['a', null, 'b', null]::text[]`,
['a', null, 'b', null]
['a', undefined, 'b', undefined]
);
testType<(string | null)[][]>(
testType<(string | undefined)[][]>(
DataType.ArrayText,
`ARRAY[ARRAY['a',null,'b'],ARRAY[null, 'c', null]]::text[][]`,
[['a', null, 'b'], [null, 'c', null]]
[['a', undefined, 'b'], [undefined, 'c', undefined]]
);
testType<Date[]>(
DataType.ArrayDate,
Expand Down Expand Up @@ -280,41 +280,41 @@ describe('Types', () => {
'ARRAY[\'{"foo": "bar"}\'::json]',
[{ 'foo': 'bar' }]);
// Test nulls
testType<boolean | null>(
testType<boolean | undefined>(
DataType.Bool,
'null::bool',
null
undefined
);
testType<string | null>(
testType<string | undefined>(
DataType.Uuid,
'null::uuid',
null
undefined
);
testType<string | null>(
testType<string | undefined>(
DataType.Text,
'null::text',
null
undefined
);
testType<string[] | null>(
testType<string[] | undefined>(
DataType.ArrayText,
'null::text[]',
null
undefined
);
testType<string[][] | null>(
testType<string[][] | undefined>(
DataType.ArrayText,
'null::text[][]',
null
undefined
);
testType<Date | null>(
testType<Date | undefined>(
DataType.ArrayTimestamptz,
'null::timestamptz',
null);
testType<Date[] | null>(
undefined);
testType<Date[] | undefined>(
DataType.ArrayTimestamptz,
'null::timestamptz[]',
null);
testType<Date[] | null>(
undefined);
testType<Date[] | undefined>(
DataType.ArrayTimestamptz,
'null::timestamptz[][]',
null);
undefined);
});