Skip to content
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
13 changes: 9 additions & 4 deletions src/token/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ export class Result<T> {
}
}

export class NotEnoughDataError extends Error {
byteCount: number;
// This error is thrown and caught as part of normal parsing control flow:
// whenever a parser encounters an incomplete value, it throws this error,
// and the caller waits for more data to arrive before retrying.
//
// It deliberately does not extend `Error` - capturing a stack trace on
// every construction is expensive, and the stack is never used as this
// error never escapes the parsing internals.
export class NotEnoughDataError {
declare byteCount: number;

constructor(byteCount: number) {
super();

this.byteCount = byteCount;
}
}
Expand Down
50 changes: 50 additions & 0 deletions test/unit/token/helpers-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { assert } from 'chai';

import { NotEnoughDataError, readBigInt64LE, readUInt8, readUInt16LE, readUInt32LE, readUsVarChar } from '../../../src/token/helpers';

function assertThrowsNotEnoughDataError(fn: () => void, expectedByteCount: number) {
try {
fn();
} catch (err) {
assert.instanceOf(err, NotEnoughDataError);
assert.strictEqual((err as NotEnoughDataError).byteCount, expectedByteCount);
return;
}

assert.fail('expected a NotEnoughDataError to be thrown');
}

describe('NotEnoughDataError', function() {
it('should carry the number of bytes needed to retry the read', function() {
const err = new NotEnoughDataError(42);

assert.strictEqual(err.byteCount, 42);
});

it('should be thrown by read helpers when the buffer is too short', function() {
const buf = Buffer.from([0x01, 0x02]);

assertThrowsNotEnoughDataError(() => readUInt8(buf, 2), 3);
assertThrowsNotEnoughDataError(() => readUInt16LE(buf, 1), 3);
assertThrowsNotEnoughDataError(() => readUInt32LE(buf, 0), 4);
assertThrowsNotEnoughDataError(() => readBigInt64LE(buf, 0), 8);
assertThrowsNotEnoughDataError(() => readUInt32LE(buf, 1), 5);
});

it('should be thrown by variable length read helpers when the data is incomplete', function() {
// A `UsVarChar` with a length prefix of 3 characters, but only 2 characters of data.
const buf = Buffer.alloc(2 + 4);
buf.writeUInt16LE(3, 0);
buf.write('ab', 2, 'ucs2');

assertThrowsNotEnoughDataError(() => readUsVarChar(buf, 0), 2 + 6);
});

it('should not be thrown when the buffer holds enough data', function() {
const buf = Buffer.from([0x01, 0x02, 0x03, 0x04]);

const result = readUInt32LE(buf, 0);
assert.strictEqual(result.value, 0x04030201);
assert.strictEqual(result.offset, 4);
});
});
51 changes: 51 additions & 0 deletions test/unit/token/row-token-parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,57 @@ describe('Row Token Parser', function() {
});
});

describe('parsing a row delivered one byte at a time', function() {
it('should parse it correctly', async function() {
const debug = new Debug();
const colMetadata: ColumnMetadata[] = [
{
colName: 'col0',
userType: 0,
flags: 0,
precision: undefined,
scale: undefined,
dataLength: undefined,
schema: undefined,
udtInfo: undefined,
type: dataTypeByName.VarChar,
collation: new Collation(1033, 0, 0, 52)
},
{
colName: 'col1',
userType: 0,
flags: 0,
precision: undefined,
scale: undefined,
dataLength: undefined,
schema: undefined,
udtInfo: undefined,
type: dataTypeByName.Int,
collation: undefined
}
];

const buffer = new WritableTrackingBuffer(0, 'ascii');
buffer.writeUInt8(0xd1);
buffer.writeUsVarchar('hello world');
buffer.writeUInt32LE(1234);

const chunks = Array.from(buffer.data, (byte) => Buffer.from([byte]));

const parser = Parser.parseTokens(chunks, debug, options, colMetadata);
const result = await parser.next();
assert.isFalse(result.done);
const token = result.value;

assert.instanceOf(token, RowToken);
assert.strictEqual(token.columns.length, 2);
assert.strictEqual(token.columns[0].value, 'hello world');
assert.strictEqual(token.columns[1].value, 1234);

assert.isTrue((await parser.next()).done);
});
});

it('should parse int', async function() {
const debug = new Debug();
const colMetadata: ColumnMetadata[] = [{
Expand Down
12 changes: 12 additions & 0 deletions test/unit/token/token-stream-parser-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,16 @@ describe('Token Stream Parser', () => {

parser.on('end', done);
});

it('should parse token delivered one byte at a time', function(done) {
const debug = new Debug({ token: true });
const buffer = createDbChangeBuffer();

const chunks = Array.from(buffer, (byte) => Buffer.from([byte]));

// Cast to Message since tests use a simplified input instead of full Message
const parser = new Parser(chunks as unknown as Message, debug, new TestDatabaseChangeHandler(), options);

parser.on('end', done);
});
});
Loading