Skip to content

Commit

Permalink
Merge pull request #18 from bartlomieju/decode_data_row
Browse files Browse the repository at this point in the history
First pass at decoding data rows
  • Loading branch information
bartlomieju committed Feb 10, 2019
2 parents 822e49a + 20c62de commit 29fa042
Show file tree
Hide file tree
Showing 5 changed files with 375 additions and 24 deletions.
20 changes: 7 additions & 13 deletions connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { parseError } from "./error.ts";
import { ConnectionParams } from "./connection_params.ts";


enum Format {
export enum Format {
TEXT = 0,
BINARY = 1,
}
Expand All @@ -60,7 +60,7 @@ export class Message {
}


class Column {
export class Column {
constructor(
public name: string,
public tableOid: number,
Expand Down Expand Up @@ -204,8 +204,6 @@ export class Connection {
if (responseCode !== 0) {
throw new Error(`Unexpected auth response code: ${responseCode}.`);
}

console.log('read auth ok!');
}

private async _authCleartext() {
Expand Down Expand Up @@ -312,7 +310,7 @@ export class Connection {
// data row
case "D":
// this is actually packet read
const foo = this._readDataRow(msg, Format.TEXT);
const foo = this._readDataRow(msg);
result.handleDataRow(foo)
break;
// command complete
Expand Down Expand Up @@ -512,7 +510,7 @@ export class Connection {
// data row
case "D":
// this is actually packet read
const rawDataRow = this._readDataRow(msg, Format.TEXT);
const rawDataRow = this._readDataRow(msg);
result.handleDataRow(rawDataRow)
break;
// command complete
Expand Down Expand Up @@ -563,7 +561,7 @@ export class Connection {
return new RowDescription(columnCount, columns);
}

_readDataRow(msg: Message, format: Format): any[] {
_readDataRow(msg: Message): any[] {
const fieldCount = msg.reader.readInt16();
const row = [];

Expand All @@ -575,12 +573,8 @@ export class Connection {
continue;
}

if (format === Format.TEXT) {
const foo = msg.reader.readString(colLength);
row.push(foo)
} else {
row.push(msg.reader.readBytes(colLength))
}
// reading raw bytes here, they will be properly parsed later
row.push(msg.reader.readBytes(colLength))
}

return row;
Expand Down
169 changes: 169 additions & 0 deletions decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Oid } from "./oid.ts";
import { Column, Format } from "./connection.ts";


// Datetime parsing based on:
// https://github.com/bendrucker/postgres-date/blob/master/index.js
const DATETIME_RE = /^(\d{1,})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?/;
const DATE_RE = /^(\d{1,})-(\d{2})-(\d{2})$/;
const TIMEZONE_RE = /([Z+-])(\d{2})?:?(\d{2})?:?(\d{2})?/;
const BC_RE = /BC$/;

function decodeDate(dateStr: string): null | Date {
const matches = DATE_RE.exec(dateStr);

if (!matches) {
return null;
}

const year = parseInt(matches[1], 10);
// remember JS dates are 0-based
const month = parseInt(matches[2], 10) - 1;
const day = parseInt(matches[3], 10);
const date = new Date(year, month, day);
// use `setUTCFullYear` because if date is from first
// century `Date`'s compatibility for millenium bug
// would set it as 19XX
date.setUTCFullYear(year);

return date;
}
/**
* Decode numerical timezone offset from provided date string.
*
* Matched these kinds:
* - `Z (UTC)`
* - `-05`
* - `+06:30`
* - `+06:30:10`
*
* Returns offset in miliseconds.
*/
function decodeTimezoneOffset(dateStr: string): null | number {
// get rid of date part as TIMEZONE_RE would match '-MM` part
const timeStr = dateStr.split(' ')[1];
const matches = TIMEZONE_RE.exec(timeStr);

if (!matches) {
return null;
}

const type = matches[1];

if (type === "Z") {
// Zulu timezone === UTC === 0
return 0;
}

// in JS timezone offsets are reversed, ie. timezones
// that are "positive" (+01:00) are represented as negative
// offsets and vice-versa
const sign = type === '-' ? 1 : -1;

const hours = parseInt(matches[2], 10);
const minutes = parseInt(matches[3] || "0", 10);
const seconds = parseInt(matches[4] || "0", 10);

const offset = (hours * 3600) + (minutes * 60) + seconds;

return sign * offset * 1000;
}

function decodeDatetime(dateStr: string): null | number | Date {
/**
* Postgres uses ISO 8601 style date output by default:
* 1997-12-17 07:37:16-08
*/

// there are special `infinity` and `-infinity`
// cases representing out-of-range dates
if (dateStr === 'infinity') {
return Number(Infinity);
} else if (dateStr === "-infinity") {
return Number(-Infinity);
}

const matches = DATETIME_RE.exec(dateStr);

if (!matches) {
return decodeDate(dateStr);
}

const isBC = BC_RE.test(dateStr);


const year = parseInt(matches[1], 10) * (isBC ? -1 : 1);
// remember JS dates are 0-based
const month = parseInt(matches[2], 10) - 1;
const day = parseInt(matches[3], 10);
const hour = parseInt(matches[4], 10);
const minute = parseInt(matches[5], 10);
const second = parseInt(matches[6], 10);
// ms are written as .007
const msMatch = matches[7];
const ms = msMatch ? 1000 * parseFloat(msMatch) : 0;


let date: Date;

const offset = decodeTimezoneOffset(dateStr);
if (offset === null) {
date = new Date(year, month, day, hour, minute, second, ms);
} else {
// This returns miliseconds from 1 January, 1970, 00:00:00,
// adding decoded timezone offset will construct proper date object.
const utc = Date.UTC(year, month, day, hour, minute, second, ms);
date = new Date(utc + offset);
}

// use `setUTCFullYear` because if date is from first
// century `Date`'s compatibility for millenium bug
// would set it as 19XX
date.setUTCFullYear(year);
return date;
}

function decodeBinary() {
throw new Error("Not implemented!")
}

const decoder = new TextDecoder();

function decodeText(value: Uint8Array, typeOid: number): any {
const strValue = decoder.decode(value);

switch (typeOid) {
case Oid.char:
case Oid.varchar:
case Oid.text:
case Oid.time:
case Oid.timetz:
return strValue;
case Oid.bool:
return strValue[0] === "t";
case Oid.int2:
case Oid.int4:
case Oid.int8:
return parseInt(strValue, 10);
case Oid.float4:
case Oid.float8:
return parseFloat(strValue);
case Oid.timestamptz:
case Oid.timestamp:
return decodeDatetime(strValue);
case Oid.date:
return decodeDate(strValue);
default:
throw new Error(`Don't know how to parse column type: ${typeOid}`);
}
}

export function decode(value: Uint8Array, column: Column) {
if (column.format === Format.BINARY) {
return decodeBinary();
} else if (column.format === Format.TEXT) {
return decodeText(value, column.typeOid);
} else {
throw new Error(`Unknown column format: ${column.format}`);
}
}
Loading

0 comments on commit 29fa042

Please sign in to comment.