Skip to content

Commit 29fa042

Browse files
authored
Merge pull request #18 from bartlomieju/decode_data_row
First pass at decoding data rows
2 parents 822e49a + 20c62de commit 29fa042

File tree

5 files changed

+375
-24
lines changed

5 files changed

+375
-24
lines changed

connection.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { parseError } from "./error.ts";
3636
import { ConnectionParams } from "./connection_params.ts";
3737

3838

39-
enum Format {
39+
export enum Format {
4040
TEXT = 0,
4141
BINARY = 1,
4242
}
@@ -60,7 +60,7 @@ export class Message {
6060
}
6161

6262

63-
class Column {
63+
export class Column {
6464
constructor(
6565
public name: string,
6666
public tableOid: number,
@@ -204,8 +204,6 @@ export class Connection {
204204
if (responseCode !== 0) {
205205
throw new Error(`Unexpected auth response code: ${responseCode}.`);
206206
}
207-
208-
console.log('read auth ok!');
209207
}
210208

211209
private async _authCleartext() {
@@ -312,7 +310,7 @@ export class Connection {
312310
// data row
313311
case "D":
314312
// this is actually packet read
315-
const foo = this._readDataRow(msg, Format.TEXT);
313+
const foo = this._readDataRow(msg);
316314
result.handleDataRow(foo)
317315
break;
318316
// command complete
@@ -512,7 +510,7 @@ export class Connection {
512510
// data row
513511
case "D":
514512
// this is actually packet read
515-
const rawDataRow = this._readDataRow(msg, Format.TEXT);
513+
const rawDataRow = this._readDataRow(msg);
516514
result.handleDataRow(rawDataRow)
517515
break;
518516
// command complete
@@ -563,7 +561,7 @@ export class Connection {
563561
return new RowDescription(columnCount, columns);
564562
}
565563

566-
_readDataRow(msg: Message, format: Format): any[] {
564+
_readDataRow(msg: Message): any[] {
567565
const fieldCount = msg.reader.readInt16();
568566
const row = [];
569567

@@ -575,12 +573,8 @@ export class Connection {
575573
continue;
576574
}
577575

578-
if (format === Format.TEXT) {
579-
const foo = msg.reader.readString(colLength);
580-
row.push(foo)
581-
} else {
582-
row.push(msg.reader.readBytes(colLength))
583-
}
576+
// reading raw bytes here, they will be properly parsed later
577+
row.push(msg.reader.readBytes(colLength))
584578
}
585579

586580
return row;

decode.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { Oid } from "./oid.ts";
2+
import { Column, Format } from "./connection.ts";
3+
4+
5+
// Datetime parsing based on:
6+
// https://github.com/bendrucker/postgres-date/blob/master/index.js
7+
const DATETIME_RE = /^(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?/;
8+
const DATE_RE = /^(\d{1,})-(\d{2})-(\d{2})$/;
9+
const TIMEZONE_RE = /([Z+-])(\d{2})?:?(\d{2})?:?(\d{2})?/;
10+
const BC_RE = /BC$/;
11+
12+
function decodeDate(dateStr: string): null | Date {
13+
const matches = DATE_RE.exec(dateStr);
14+
15+
if (!matches) {
16+
return null;
17+
}
18+
19+
const year = parseInt(matches[1], 10);
20+
// remember JS dates are 0-based
21+
const month = parseInt(matches[2], 10) - 1;
22+
const day = parseInt(matches[3], 10);
23+
const date = new Date(year, month, day);
24+
// use `setUTCFullYear` because if date is from first
25+
// century `Date`'s compatibility for millenium bug
26+
// would set it as 19XX
27+
date.setUTCFullYear(year);
28+
29+
return date;
30+
}
31+
/**
32+
* Decode numerical timezone offset from provided date string.
33+
*
34+
* Matched these kinds:
35+
* - `Z (UTC)`
36+
* - `-05`
37+
* - `+06:30`
38+
* - `+06:30:10`
39+
*
40+
* Returns offset in miliseconds.
41+
*/
42+
function decodeTimezoneOffset(dateStr: string): null | number {
43+
// get rid of date part as TIMEZONE_RE would match '-MM` part
44+
const timeStr = dateStr.split(' ')[1];
45+
const matches = TIMEZONE_RE.exec(timeStr);
46+
47+
if (!matches) {
48+
return null;
49+
}
50+
51+
const type = matches[1];
52+
53+
if (type === "Z") {
54+
// Zulu timezone === UTC === 0
55+
return 0;
56+
}
57+
58+
// in JS timezone offsets are reversed, ie. timezones
59+
// that are "positive" (+01:00) are represented as negative
60+
// offsets and vice-versa
61+
const sign = type === '-' ? 1 : -1;
62+
63+
const hours = parseInt(matches[2], 10);
64+
const minutes = parseInt(matches[3] || "0", 10);
65+
const seconds = parseInt(matches[4] || "0", 10);
66+
67+
const offset = (hours * 3600) + (minutes * 60) + seconds;
68+
69+
return sign * offset * 1000;
70+
}
71+
72+
function decodeDatetime(dateStr: string): null | number | Date {
73+
/**
74+
* Postgres uses ISO 8601 style date output by default:
75+
* 1997-12-17 07:37:16-08
76+
*/
77+
78+
// there are special `infinity` and `-infinity`
79+
// cases representing out-of-range dates
80+
if (dateStr === 'infinity') {
81+
return Number(Infinity);
82+
} else if (dateStr === "-infinity") {
83+
return Number(-Infinity);
84+
}
85+
86+
const matches = DATETIME_RE.exec(dateStr);
87+
88+
if (!matches) {
89+
return decodeDate(dateStr);
90+
}
91+
92+
const isBC = BC_RE.test(dateStr);
93+
94+
95+
const year = parseInt(matches[1], 10) * (isBC ? -1 : 1);
96+
// remember JS dates are 0-based
97+
const month = parseInt(matches[2], 10) - 1;
98+
const day = parseInt(matches[3], 10);
99+
const hour = parseInt(matches[4], 10);
100+
const minute = parseInt(matches[5], 10);
101+
const second = parseInt(matches[6], 10);
102+
// ms are written as .007
103+
const msMatch = matches[7];
104+
const ms = msMatch ? 1000 * parseFloat(msMatch) : 0;
105+
106+
107+
let date: Date;
108+
109+
const offset = decodeTimezoneOffset(dateStr);
110+
if (offset === null) {
111+
date = new Date(year, month, day, hour, minute, second, ms);
112+
} else {
113+
// This returns miliseconds from 1 January, 1970, 00:00:00,
114+
// adding decoded timezone offset will construct proper date object.
115+
const utc = Date.UTC(year, month, day, hour, minute, second, ms);
116+
date = new Date(utc + offset);
117+
}
118+
119+
// use `setUTCFullYear` because if date is from first
120+
// century `Date`'s compatibility for millenium bug
121+
// would set it as 19XX
122+
date.setUTCFullYear(year);
123+
return date;
124+
}
125+
126+
function decodeBinary() {
127+
throw new Error("Not implemented!")
128+
}
129+
130+
const decoder = new TextDecoder();
131+
132+
function decodeText(value: Uint8Array, typeOid: number): any {
133+
const strValue = decoder.decode(value);
134+
135+
switch (typeOid) {
136+
case Oid.char:
137+
case Oid.varchar:
138+
case Oid.text:
139+
case Oid.time:
140+
case Oid.timetz:
141+
return strValue;
142+
case Oid.bool:
143+
return strValue[0] === "t";
144+
case Oid.int2:
145+
case Oid.int4:
146+
case Oid.int8:
147+
return parseInt(strValue, 10);
148+
case Oid.float4:
149+
case Oid.float8:
150+
return parseFloat(strValue);
151+
case Oid.timestamptz:
152+
case Oid.timestamp:
153+
return decodeDatetime(strValue);
154+
case Oid.date:
155+
return decodeDate(strValue);
156+
default:
157+
throw new Error(`Don't know how to parse column type: ${typeOid}`);
158+
}
159+
}
160+
161+
export function decode(value: Uint8Array, column: Column) {
162+
if (column.format === Format.BINARY) {
163+
return decodeBinary();
164+
} else if (column.format === Format.TEXT) {
165+
return decodeText(value, column.typeOid);
166+
} else {
167+
throw new Error(`Unknown column format: ${column.format}`);
168+
}
169+
}

0 commit comments

Comments
 (0)