diff --git a/src/bulk-load.ts b/src/bulk-load.ts
index fe47e4fff..9aa0bfb64 100644
--- a/src/bulk-load.ts
+++ b/src/bulk-load.ts
@@ -5,8 +5,8 @@ import Connection, { type InternalConnectionOptions } from './connection';
import { Transform } from 'stream';
import { TYPE as TOKEN_TYPE } from './token/token';
-import { type DataType, type Parameter } from './data-type';
-import { Collation } from './collation';
+import { TYPES, type DataType, type Parameter } from './data-type';
+import { Collation, JSON_COLLATION } from './collation';
/**
* @private
@@ -563,7 +563,21 @@ class BulkLoad extends EventEmitter {
tBuf.writeUInt16LE(flags);
// TYPE_INFO
- tBuf.writeBuffer(c.type.generateTypeInfo(c, this.options));
+ if (c.type === TYPES.JSON) {
+ // The server rejects the `json` data type (0xF4) in bulk load column
+ // metadata ("Invalid column type from bcp client"), even when
+ // JSONSUPPORT was negotiated. Substitute `varchar(max)` with the
+ // fixed `json` collation instead - the values are PLP-encoded UTF-8
+ // either way, and the server converts them to `json` based on the
+ // column's type in the `insert bulk` statement. SqlClient performs
+ // the same substitution, but with a zeroed collation - the server
+ // ignores the collation here (the `insert bulk` statement pins the
+ // interpretation to UTF-8), so send the truthful one for consistency
+ // with the TVP substitution, where it is required.
+ tBuf.writeBuffer(TYPES.VarChar.generateTypeInfo({ length: Infinity, collation: JSON_COLLATION, value: null }, this.options));
+ } else {
+ tBuf.writeBuffer(c.type.generateTypeInfo(c, this.options));
+ }
// TableName
if (c.type.hasTableName) {
diff --git a/src/collation.ts b/src/collation.ts
index b3f045153..f7a3ec3e4 100644
--- a/src/collation.ts
+++ b/src/collation.ts
@@ -352,3 +352,11 @@ export class Collation {
return this.buffer;
}
}
+
+/**
+ * `Latin1_General_100_BIN2_UTF8`, the collation the `json` data type is fixed
+ * to. Sent in the `varchar(max)` TYPE_INFO that is substituted for `json`
+ * columns in bulk loads and TVPs, so that the server decodes their
+ * PLP-encoded data as UTF-8.
+ */
+export const JSON_COLLATION = new Collation(0x0409, Flags.BINARY2 | Flags.UTF8, 2, 0);
diff --git a/src/connection.ts b/src/connection.ts
index 0ff2f90e3..49175cb85 100644
--- a/src/connection.ts
+++ b/src/connection.ts
@@ -1069,6 +1069,15 @@ class Connection extends EventEmitter {
*/
declare databaseCollation: Collation | undefined;
+ /**
+ * Whether the server acknowledged the JSON_SUPPORT feature extension,
+ * i.e. whether the `json` data type can be sent to and received from
+ * the server.
+ *
+ * @private
+ */
+ declare serverSupportsJson: boolean;
+
/**
* @private
*/
@@ -1805,6 +1814,7 @@ class Connection extends EventEmitter {
this.state = this.STATE.INITIALIZED;
this.attentionSent = false;
+ this.serverSupportsJson = false;
this._cancelAfterRequestSent = () => {
this.messageIo.sendMessage(TYPE.ATTENTION);
@@ -2476,6 +2486,10 @@ class Connection extends EventEmitter {
* @private
*/
sendLogin7Packet() {
+ // A new login attempt (e.g. after a transient failure or rerouting)
+ // must re-negotiate JSON support from scratch.
+ this.serverSupportsJson = false;
+
const payload = new Login7Payload({
tdsVersion: versions[this.config.options.tdsVersion],
packetSize: this.config.options.packetSize,
@@ -3209,6 +3223,19 @@ class Connection extends EventEmitter {
process.nextTick(() => {
request.callback(new RequestError('Canceled.', 'ECANCEL'));
});
+ } else if (payload instanceof RpcRequestPayload && !this.serverSupportsJson && payload.parameters.some((parameter) => parameter.type === TYPES.JSON)) {
+ let message;
+ if (versions[this.config.options.tdsVersion] < versions['7_4']) {
+ // JSON support was never requested at login - feature extensions
+ // (and with them the JSONSUPPORT negotiation) require TDS 7.4.
+ message = 'JSON support was not negotiated with the server because the configured `tdsVersion` (`' + this.config.options.tdsVersion + '`) is lower than `7_4`. `TYPES.JSON` parameters require TDS version `7_4`.';
+ } else {
+ message = 'The server does not support the `json` data type. `TYPES.JSON` parameters require SQL Server 2025 (or newer) or Azure SQL with JSON support enabled.';
+ }
+ this.debug.log(message);
+ process.nextTick(() => {
+ request.callback(new RequestError(message, 'EJSONNOTSUPPORTED'));
+ });
} else {
if (packetType === TYPE.SQL_BATCH) {
this.isSqlBatch = true;
diff --git a/src/data-type.ts b/src/data-type.ts
index b39adce22..447df1e4b 100644
--- a/src/data-type.ts
+++ b/src/data-type.ts
@@ -41,6 +41,7 @@ import { type CryptoMetadata } from './always-encrypted/types';
import { type InternalConnectionOptions } from './connection';
import { Collation } from './collation';
+import Json from './data-types/json';
export interface Parameter {
type: DataType;
@@ -129,6 +130,7 @@ export const TYPE = {
[UDT.id]: UDT,
[TVP.id]: TVP,
[Variant.id]: Variant,
+ [Json.id]: Json
};
/**
@@ -400,6 +402,13 @@ export const TYPE = {
*
✓ |
* - |
*
+ *
+ * json |
+ * [[TYPES.JSON]] |
+ * string|object |
+ * ✓ |
+ * - |
+ *
*
*
*
@@ -467,7 +476,8 @@ export const TYPES = {
DateTimeOffset,
UDT,
TVP,
- Variant
+ Variant,
+ JSON: Json
};
export const typeByName = TYPES;
diff --git a/src/data-types/json.ts b/src/data-types/json.ts
new file mode 100644
index 000000000..95149ab61
--- /dev/null
+++ b/src/data-types/json.ts
@@ -0,0 +1,72 @@
+import { type DataType } from '../data-type';
+
+const UNKNOWN_PLP_LEN = Buffer.from([0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]);
+const PLP_TERMINATOR = Buffer.from([0x00, 0x00, 0x00, 0x00]);
+const MAX_NULL_LENGTH = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
+
+const Json: DataType = {
+ id: 0xF4,
+ type: 'JSON',
+ name: 'JSON',
+
+ declaration: function() {
+ return 'json';
+ },
+
+ generateTypeInfo() {
+ return Buffer.from([this.id]);
+ },
+
+ generateParameterLength(parameter, options) {
+ const value = parameter.value as Buffer | null;
+
+ if (value == null) {
+ return MAX_NULL_LENGTH;
+ }
+
+ return UNKNOWN_PLP_LEN;
+ },
+
+ *generateParameterData(parameter, options) {
+ const value = parameter.value as Buffer | null;
+
+ if (value == null) {
+ return;
+ }
+
+ if (value.length > 0) {
+ const buffer = Buffer.alloc(4);
+ buffer.writeUInt32LE(value.length, 0);
+ yield buffer;
+
+ yield value;
+ }
+
+ yield PLP_TERMINATOR;
+ },
+
+ validate: function(value): Buffer | null {
+ if (value == null) {
+ return null;
+ }
+
+ if (typeof value === 'string') {
+ JSON.parse(value);
+ return Buffer.from(value, 'utf8');
+ }
+
+ if (Buffer.isBuffer(value)) {
+ throw new TypeError('Invalid JSON value.');
+ }
+
+ const serialized = JSON.stringify(value);
+ if (serialized === undefined) {
+ throw new TypeError('Invalid JSON value.');
+ }
+
+ return Buffer.from(serialized, 'utf8');
+ }
+};
+
+export default Json;
+module.exports = Json;
diff --git a/src/data-types/tvp.ts b/src/data-types/tvp.ts
index e1de96005..adc0f23bb 100644
--- a/src/data-types/tvp.ts
+++ b/src/data-types/tvp.ts
@@ -1,6 +1,9 @@
import { type DataType } from '../data-type';
import { InputError } from '../errors';
import WritableTrackingBuffer from '../tracking-buffer/writable-tracking-buffer';
+import { JSON_COLLATION } from '../collation';
+import Json from './json';
+import VarChar from './varchar';
const TVP_ROW_TOKEN = Buffer.from([0x01]);
const TVP_END_TOKEN = Buffer.from([0x00]);
@@ -69,7 +72,19 @@ const TVP: DataType = {
yield buff;
// TYPE_INFO
- yield column.type.generateTypeInfo(column);
+ if (column.type === Json) {
+ // The server can not handle the `json` data type (0xF4) in TVP column
+ // metadata - it kills the session. Substitute `varchar(max)` with the
+ // fixed `json` collation instead - the values are PLP-encoded UTF-8
+ // either way, and the server converts them to `json` based on the
+ // table type's column definition. This matches the substitution
+ // performed for bulk loads. The collation is required here: with a
+ // zeroed collation, the server decodes the data using its default
+ // codepage instead of UTF-8.
+ yield VarChar.generateTypeInfo({ length: Infinity, collation: JSON_COLLATION, value: null }, options);
+ } else {
+ yield column.type.generateTypeInfo(column);
+ }
// ColName
yield Buffer.from([0x00]);
diff --git a/src/login7-payload.ts b/src/login7-payload.ts
index 923da8102..1dbb3030c 100644
--- a/src/login7-payload.ts
+++ b/src/login7-payload.ts
@@ -444,6 +444,17 @@ class Login7Payload {
buf.writeUInt8(UTF8_SUPPORT_CLIENT_SUPPORTS_UTF8, 5);
buffers.push(buf);
+ // Signal JSON support: Value 0x0D, the single data byte is the highest
+ // JSON version the client supports. The server only sends or accepts the
+ // `json` data type (0xF4) after acknowledging this feature.
+ const JSON_SUPPORT_FEATURE_ID = 0x0d;
+ const JSON_SUPPORT_MAX_VERSION = 0x01;
+ const jsonSupportBuf = Buffer.alloc(6);
+ jsonSupportBuf.writeUInt8(JSON_SUPPORT_FEATURE_ID, 0);
+ jsonSupportBuf.writeUInt32LE(1, 1);
+ jsonSupportBuf.writeUInt8(JSON_SUPPORT_MAX_VERSION, 5);
+ buffers.push(jsonSupportBuf);
+
buffers.push(Buffer.from([FEATURE_EXT_TERMINATOR]));
return Buffer.concat(buffers);
diff --git a/src/metadata-parser.ts b/src/metadata-parser.ts
index 8dc33e801..5810e9196 100644
--- a/src/metadata-parser.ts
+++ b/src/metadata-parser.ts
@@ -272,6 +272,21 @@ function readMetadata(buf: Buffer, offset: number, options: ParserOptions): Resu
}, offset);
}
+ case 'JSON':
+ // The `json` TYPE_INFO carries no additional metadata — no length
+ // and no collation (the collation is fixed to UTF-8 per RFC 8259).
+ return new Result({
+ userType: userType,
+ flags: flags,
+ type: type,
+ collation: undefined,
+ precision: undefined,
+ scale: undefined,
+ dataLength: undefined,
+ schema: undefined,
+ udtInfo: undefined
+ }, offset);
+
case 'Xml': {
let schema;
({ offset, value: schema } = readSchema(buf, offset));
diff --git a/src/token/feature-ext-ack-parser.ts b/src/token/feature-ext-ack-parser.ts
index 0e6a5d034..d0b781c88 100644
--- a/src/token/feature-ext-ack-parser.ts
+++ b/src/token/feature-ext-ack-parser.ts
@@ -10,19 +10,21 @@ const FEATURE_ID = {
GLOBALTRANSACTIONS: 0x05,
AZURESQLSUPPORT: 0x08,
UTF8_SUPPORT: 0x0A,
+ JSON_SUPPORT: 0x0D,
TERMINATOR: 0xFF
};
function featureExtAckParser(buf: Buffer, offset: number, _options: ParserOptions): Result {
let fedAuth: Buffer | undefined;
let utf8Support: boolean | undefined;
+ let jsonSupport: Buffer | undefined;
while (true) {
let featureId;
({ value: featureId, offset } = readUInt8(buf, offset));
if (featureId === FEATURE_ID.TERMINATOR) {
- return new Result(new FeatureExtAckToken(fedAuth, utf8Support), offset);
+ return new Result(new FeatureExtAckToken(fedAuth, utf8Support, jsonSupport), offset);
}
let featureAckDataLen;
@@ -42,6 +44,12 @@ function featureExtAckParser(buf: Buffer, offset: number, _options: ParserOption
case FEATURE_ID.UTF8_SUPPORT:
utf8Support = !!featureData[0];
break;
+ case FEATURE_ID.JSON_SUPPORT:
+ // The single data byte is the JSON version chosen by the server.
+ // Whether that version is one this client supports is decided by the
+ // Login7TokenHandler.
+ jsonSupport = featureData;
+ break;
}
}
}
diff --git a/src/token/handler.ts b/src/token/handler.ts
index 1d0174f41..fdda37a50 100644
--- a/src/token/handler.ts
+++ b/src/token/handler.ts
@@ -310,13 +310,25 @@ export class Login7TokenHandler extends TokenHandler {
onFeatureExtAck(token: FeatureExtAckToken) {
const { authentication } = this.connection.config;
+ if (token.jsonSupport !== undefined) {
+ if (token.jsonSupport.length === 1 && token.jsonSupport[0] === 0x01) {
+ this.connection.serverSupportsJson = true;
+ } else {
+ // The server acknowledged a JSON version this client did not offer.
+ // It will use that version's wire format for `json` values, which
+ // this client can not safely parse - fail the login instead of
+ // continuing on a connection whose data can not be trusted.
+ this.loginError = new ConnectionError('Received invalid JSON support acknowledgement');
+ }
+ }
+
if (authentication.type === 'azure-active-directory-password' || authentication.type === 'azure-active-directory-access-token' || authentication.type === 'azure-active-directory-msi-vm' || authentication.type === 'azure-active-directory-msi-app-service' || authentication.type === 'azure-active-directory-service-principal-secret' || authentication.type === 'azure-active-directory-default') {
if (token.fedAuth === undefined) {
this.loginError = new ConnectionError('Did not receive Active Directory authentication acknowledgement');
} else if (token.fedAuth.length !== 0) {
this.loginError = new ConnectionError(`Active Directory authentication acknowledgment for ${authentication.type} authentication method includes extra data`);
}
- } else if (token.fedAuth === undefined && token.utf8Support === undefined) {
+ } else if (token.fedAuth === undefined && token.utf8Support === undefined && token.jsonSupport === undefined) {
this.loginError = new ConnectionError('Received acknowledgement for unknown feature');
} else if (token.fedAuth) {
this.loginError = new ConnectionError('Did not request Active Directory authentication, but received the acknowledgment');
diff --git a/src/token/nbcrow-token-parser.ts b/src/token/nbcrow-token-parser.ts
index 0b8d07468..6aee52f17 100644
--- a/src/token/nbcrow-token-parser.ts
+++ b/src/token/nbcrow-token-parser.ts
@@ -59,6 +59,8 @@ async function nbcRowParser(parser: Parser): Promise {
columns.push({ value: iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'), metadata });
} else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') {
columns.push({ value: Buffer.concat(chunks), metadata });
+ } else if (metadata.type.name === 'JSON') {
+ columns.push({ value: Buffer.concat(chunks).toString('utf8'), metadata });
}
} else {
let result;
diff --git a/src/token/returnvalue-token-parser.ts b/src/token/returnvalue-token-parser.ts
index bcfa14ced..bb3d77b8a 100644
--- a/src/token/returnvalue-token-parser.ts
+++ b/src/token/returnvalue-token-parser.ts
@@ -57,6 +57,8 @@ async function returnParser(parser: Parser): Promise {
value = iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8');
} else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') {
value = Buffer.concat(chunks);
+ } else if (metadata.type.name === 'JSON') {
+ value = Buffer.concat(chunks).toString('utf8');
}
} else {
try {
diff --git a/src/token/row-token-parser.ts b/src/token/row-token-parser.ts
index f394dbf97..251ab3b63 100644
--- a/src/token/row-token-parser.ts
+++ b/src/token/row-token-parser.ts
@@ -30,6 +30,8 @@ async function rowParser(parser: Parser): Promise {
columns.push({ value: iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'), metadata });
} else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') {
columns.push({ value: Buffer.concat(chunks), metadata });
+ } else if (metadata.type.name === 'JSON') {
+ columns.push({ value: Buffer.concat(chunks).toString('utf8'), metadata });
}
} else {
let result;
diff --git a/src/token/token.ts b/src/token/token.ts
index 222d2bf02..bc6ff491b 100644
--- a/src/token/token.ts
+++ b/src/token/token.ts
@@ -332,11 +332,18 @@ export class FeatureExtAckToken extends Token {
* undefined when UTF8_SUPPORT not included in token. */
declare utf8Support: boolean | undefined;
- constructor(fedAuth: Buffer | undefined, utf8Support: boolean | undefined) {
+ /** Raw data of the JSON_SUPPORT acknowledgement. Contains the JSON version
+ * chosen by the server as its single byte.
+ *
+ * undefined when JSON_SUPPORT not included in token. */
+ declare jsonSupport: Buffer | undefined;
+
+ constructor(fedAuth: Buffer | undefined, utf8Support: boolean | undefined, jsonSupport: Buffer | undefined) {
super('FEATUREEXTACK', 'onFeatureExtAck');
this.fedAuth = fedAuth;
this.utf8Support = utf8Support;
+ this.jsonSupport = jsonSupport;
}
}
diff --git a/src/value-parser.ts b/src/value-parser.ts
index 32ba6d1e7..14bfb8742 100644
--- a/src/value-parser.ts
+++ b/src/value-parser.ts
@@ -410,6 +410,10 @@ function isPLPStream(metadata: Metadata) {
case 'UDT': {
return true;
}
+
+ case 'JSON': {
+ return true;
+ }
}
}
diff --git a/test/integration/json-test.ts b/test/integration/json-test.ts
new file mode 100644
index 000000000..ee5c065c5
--- /dev/null
+++ b/test/integration/json-test.ts
@@ -0,0 +1,280 @@
+import { assert } from 'chai';
+
+import Connection from '../../src/connection';
+import Request from '../../src/request';
+import { typeByName as TYPES } from '../../src/data-type';
+import { debugOptionsFromEnv } from '../helpers/debug-options-from-env';
+
+import defaultConfig from '../config';
+
+const config = {
+ ...defaultConfig,
+ options: {
+ ...defaultConfig.options,
+ debug: debugOptionsFromEnv(),
+ tdsVersion: process.env.TEDIOUS_TDS_VERSION
+ }
+};
+
+describe('json data type', function() {
+ let connection: Connection;
+
+ beforeEach(function(done) {
+ connection = new Connection(config);
+
+ if (process.env.TEDIOUS_DEBUG) {
+ connection.on('debug', console.log);
+ }
+
+ connection.connect(done);
+ });
+
+ afterEach(function(done) {
+ if (!connection.closed) {
+ connection.on('end', done);
+ connection.close();
+ } else {
+ done();
+ }
+ });
+
+ describe('on servers that support the json data type', function() {
+ beforeEach(function() {
+ if (!connection.serverSupportsJson) {
+ this.skip();
+ }
+ });
+
+ it('returns json columns as strings', function(done) {
+ const request = new Request('SELECT CAST(\'{"a":[1,"ü"]}\' AS json)', (err) => {
+ done(err);
+ });
+
+ request.on('row', (columns) => {
+ assert.strictEqual(columns[0].metadata.type.name, 'JSON');
+ assert.isString(columns[0].value);
+ assert.deepEqual(JSON.parse(columns[0].value), { a: [1, 'ü'] });
+ });
+
+ connection.execSql(request);
+ });
+
+ it('returns null json values as `null`', function(done) {
+ const request = new Request('SELECT CAST(NULL AS json)', (err) => {
+ done(err);
+ });
+
+ request.on('row', (columns) => {
+ assert.strictEqual(columns[0].metadata.type.name, 'JSON');
+ assert.isNull(columns[0].value);
+ });
+
+ connection.execSql(request);
+ });
+
+ it('round-trips string parameter values', function(done) {
+ const value = '{"a":[1,"ü"],"b":null}';
+
+ const request = new Request('SELECT @p', (err) => {
+ done(err);
+ });
+ request.addParameter('p', TYPES.JSON, value);
+
+ request.on('row', (columns) => {
+ assert.deepEqual(JSON.parse(columns[0].value), JSON.parse(value));
+ });
+
+ connection.execSql(request);
+ });
+
+ it('round-trips object parameter values', function(done) {
+ const value = { a: [1, 'ü'], b: null };
+
+ const request = new Request('SELECT @p', (err) => {
+ done(err);
+ });
+ request.addParameter('p', TYPES.JSON, value);
+
+ request.on('row', (columns) => {
+ assert.deepEqual(JSON.parse(columns[0].value), value);
+ });
+
+ connection.execSql(request);
+ });
+
+ it('round-trips `null` parameter values', function(done) {
+ const request = new Request('SELECT @p', (err) => {
+ done(err);
+ });
+ request.addParameter('p', TYPES.JSON, null);
+
+ request.on('row', (columns) => {
+ assert.isNull(columns[0].value);
+ });
+
+ connection.execSql(request);
+ });
+
+ it('bulk loads json values', function(done) {
+ const bulkLoad = connection.newBulkLoad('#tedious_json_bulk', (err, rowCount) => {
+ if (err) {
+ return done(err);
+ }
+
+ assert.strictEqual(rowCount, 3);
+
+ const values: unknown[] = [];
+ const request = new Request('SELECT [value] FROM #tedious_json_bulk ORDER BY [id]', (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ assert.deepEqual(JSON.parse(values[0] as string), { a: [1, 'ü'] });
+ assert.deepEqual(JSON.parse(values[1] as string), { b: 2 });
+ assert.isNull(values[2]);
+ done();
+ });
+
+ request.on('row', (columns) => {
+ values.push(columns[0].value);
+ });
+
+ connection.execSqlBatch(request);
+ });
+
+ bulkLoad.addColumn('id', TYPES.Int, { nullable: false });
+ bulkLoad.addColumn('value', TYPES.JSON, { nullable: true });
+
+ const createTable = new Request('CREATE TABLE #tedious_json_bulk ([id] int NOT NULL, [value] json NULL)', (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ connection.execBulkLoad(bulkLoad, [
+ [1, '{"a":[1,"ü"]}'],
+ [2, { b: 2 }],
+ [3, null]
+ ]);
+ });
+
+ connection.execSqlBatch(createTable);
+ });
+
+ it('round-trips json values in table-valued parameters', function(done) {
+ const createType = new Request('DROP TYPE IF EXISTS [__tediousJsonTvpType]; CREATE TYPE [__tediousJsonTvpType] AS TABLE ([value] json NULL)', (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ const values: unknown[] = [];
+ const request = new Request('SELECT [value] FROM @tvp', (err) => {
+ const dropType = new Request('DROP TYPE IF EXISTS [__tediousJsonTvpType]', (dropErr) => {
+ if (err ?? dropErr) {
+ return done(err ?? dropErr);
+ }
+
+ assert.deepEqual(JSON.parse(values[0] as string), { a: [1, 'ü'] });
+ assert.isNull(values[1]);
+ done();
+ });
+
+ connection.execSqlBatch(dropType);
+ });
+
+ request.on('row', (columns) => {
+ values.push(columns[0].value);
+ });
+
+ request.addParameter('tvp', TYPES.TVP, {
+ name: '__tediousJsonTvpType',
+ columns: [{ name: 'value', type: TYPES.JSON }],
+ rows: [
+ ['{"a":[1,"ü"]}'],
+ [null]
+ ]
+ });
+
+ connection.execSql(request);
+ });
+
+ connection.execSqlBatch(createType);
+ });
+
+ it('returns json values for output parameters', function(done) {
+ let returnValueReceived = false;
+
+ const request = new Request('SET @out = @in', (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ assert.isTrue(returnValueReceived);
+ done();
+ });
+
+ request.addParameter('in', TYPES.JSON, '{"a":[1,"ü"]}');
+ request.addOutputParameter('out', TYPES.JSON);
+
+ request.on('returnValue', (name, value, metadata) => {
+ assert.strictEqual(name, 'out');
+ assert.deepEqual(JSON.parse(value as string), { a: [1, 'ü'] });
+ assert.strictEqual(metadata.type.name, 'JSON');
+ returnValueReceived = true;
+ });
+
+ connection.execSql(request);
+ });
+
+ it('returns `null` for json output parameters set to `null`', function(done) {
+ let returnValueReceived = false;
+
+ const request = new Request('SET @out = NULL', (err) => {
+ if (err) {
+ return done(err);
+ }
+
+ assert.isTrue(returnValueReceived);
+ done();
+ });
+
+ request.addOutputParameter('out', TYPES.JSON);
+
+ request.on('returnValue', (name, value, metadata) => {
+ assert.strictEqual(name, 'out');
+ assert.isNull(value);
+ assert.strictEqual(metadata.type.name, 'JSON');
+ returnValueReceived = true;
+ });
+
+ connection.execSql(request);
+ });
+ });
+
+ describe('on servers that do not support the json data type', function() {
+ beforeEach(function() {
+ if (connection.serverSupportsJson) {
+ this.skip();
+ }
+ });
+
+ it('fails json parameters with a descriptive error', function(done) {
+ const request = new Request('SELECT @p', (err) => {
+ assert.instanceOf(err, Error);
+ assert.strictEqual((err as any).code, 'EJSONNOTSUPPORTED');
+
+ if (config.options.tdsVersion && config.options.tdsVersion < '7_4') {
+ // JSON support was never negotiated because of the configured TDS
+ // version - the error should point at that, not at the server.
+ assert.include((err as Error).message, 'tdsVersion');
+ } else {
+ assert.include((err as Error).message, 'SQL Server 2025');
+ }
+
+ done();
+ });
+ request.addParameter('p', TYPES.JSON, '{"a":1}');
+
+ connection.execSql(request);
+ });
+ });
+});
diff --git a/test/unit/bulk-load-test.ts b/test/unit/bulk-load-test.ts
index d1bf4393a..ec9882360 100644
--- a/test/unit/bulk-load-test.ts
+++ b/test/unit/bulk-load-test.ts
@@ -1,5 +1,6 @@
import { assert } from 'chai';
import BulkLoad from '../../src/bulk-load';
+import { TYPES } from '../../src/data-type';
import { type InternalConnectionOptions } from '../../src/connection';
// Test options - using type assertion since tests only exercise code paths
@@ -12,6 +13,27 @@ describe('BulkLoad', function() {
assert.strictEqual(request.canceled, false);
});
+ describe('#getColMetaData', function() {
+ it('substitutes `varchar(max)` type info for `json` columns', function() {
+ const request = new BulkLoad('tablename', undefined, connectionOptions, { }, () => {});
+ request.addColumn('value', TYPES.JSON, { nullable: true });
+
+ const expected = Buffer.concat([
+ Buffer.from([0x81]), // COLMETADATA
+ Buffer.from([0x01, 0x00]), // column count
+ Buffer.from([0x00, 0x00, 0x00, 0x00]), // user type
+ Buffer.from([0x05, 0x00]), // flags (updateableReadWrite | nullable)
+ // The `json` data type (0xF4) is rejected by the server in bulk
+ // loads - `varchar(max)` with the Latin1_General_100_BIN2_UTF8
+ // collation is sent instead.
+ Buffer.from([0xA7, 0xFF, 0xFF, 0x09, 0x04, 0x00, 0x26, 0x00]),
+ Buffer.from('\x05v\0a\0l\0u\0e\0', 'binary') // column name
+ ]);
+
+ assert.deepEqual(request.getColMetaData(), expected);
+ });
+ });
+
describe('#cancel', function() {
it('marks the request as canceled', function() {
const request = new BulkLoad('tablename', undefined, connectionOptions, { }, () => {});
diff --git a/test/unit/data-type.ts b/test/unit/data-type.ts
index 7263f6450..ab6796c6b 100644
--- a/test/unit/data-type.ts
+++ b/test/unit/data-type.ts
@@ -1451,6 +1451,32 @@ describe('TVP', function() {
const buffer = Buffer.concat([...TYPES.TVP.generateParameterData(parameterValue, optionsWithUTCFalse)]);
assert.deepEqual(buffer, expected);
});
+
+ it('substitutes `varchar(max)` type info for `json` columns', function() {
+ const value = {
+ columns: [{ name: 'value', type: TYPES.JSON }],
+ rows: [['{"a":1}']]
+ };
+
+ const expected = Buffer.concat([
+ Buffer.from('000000000000', 'hex'), // user type + flags
+ // The `json` data type (0xF4) is substituted with `varchar(max)`
+ // and the Latin1_General_100_BIN2_UTF8 collation.
+ Buffer.from('a7ffff0904002600', 'hex'),
+ Buffer.from('00', 'hex'), // column name (always zero length)
+ Buffer.from('00', 'hex'), // end of column metadata
+ Buffer.from('01', 'hex'), // row token
+ Buffer.from('feffffffffffffff', 'hex'), // unknown PLP length
+ Buffer.from('07000000', 'hex'), // PLP chunk length
+ Buffer.from('{"a":1}', 'utf8'),
+ Buffer.from('00000000', 'hex'), // PLP terminator
+ Buffer.from('00', 'hex') // end of rows
+ ]);
+ const parameterValue = { value };
+
+ const buffer = Buffer.concat([...TYPES.TVP.generateParameterData(parameterValue, optionsWithUTCFalse)]);
+ assert.deepEqual(buffer, expected);
+ });
});
describe('.generateTypeInfo', function() {
@@ -1679,3 +1705,89 @@ describe('VarChar', function() {
});
});
});
+
+describe('JSON', function() {
+ describe('.declaration', function() {
+ it('returns "json"', function() {
+ assert.strictEqual(TYPES.JSON.declaration({ value: '{}' } as any), 'json');
+ });
+ });
+
+ describe('.generateTypeInfo', function() {
+ it('returns the JSON type token without additional metadata', function() {
+ const result = TYPES.JSON.generateTypeInfo({ value: null }, options);
+ assert.deepEqual(result, Buffer.from([0xF4]));
+ });
+ });
+
+ describe('.generateParameterLength', function() {
+ it('returns the PLP null length for `null` values', function() {
+ const result = TYPES.JSON.generateParameterLength({ value: null }, options);
+ assert.deepEqual(result, Buffer.from('ffffffffffffffff', 'hex'));
+ });
+
+ it('returns the unknown PLP length for non-null values', function() {
+ const result = TYPES.JSON.generateParameterLength({ value: Buffer.from('{"a":1}') }, options);
+ assert.deepEqual(result, Buffer.from('feffffffffffffff', 'hex'));
+ });
+ });
+
+ describe('.generateParameterData', function() {
+ it('generates no data for `null` values', function() {
+ const buffer = Buffer.concat([...TYPES.JSON.generateParameterData({ value: null }, options)]);
+ assert.deepEqual(buffer, Buffer.alloc(0));
+ });
+
+ it('generates only the PLP terminator for empty values', function() {
+ const buffer = Buffer.concat([...TYPES.JSON.generateParameterData({ value: Buffer.alloc(0) }, options)]);
+ assert.deepEqual(buffer, Buffer.from('00000000', 'hex'));
+ });
+
+ it('generates a single length-prefixed chunk followed by the PLP terminator', function() {
+ const value = Buffer.from('{"a":1}', 'utf8');
+ const buffer = Buffer.concat([...TYPES.JSON.generateParameterData({ value: value }, options)]);
+
+ const expected = Buffer.concat([
+ Buffer.from('07000000', 'hex'),
+ value,
+ Buffer.from('00000000', 'hex')
+ ]);
+ assert.deepEqual(buffer, expected);
+ });
+ });
+
+ describe('.validate', function() {
+ it('returns `null` for `null` and `undefined` values', function() {
+ assert.isNull(TYPES.JSON.validate(null, undefined));
+ assert.isNull(TYPES.JSON.validate(undefined, undefined));
+ });
+
+ it('returns the UTF-8 encoded value for strings containing valid JSON', function() {
+ const result = TYPES.JSON.validate('{"a":"ü"}', undefined);
+ assert.deepEqual(result, Buffer.from('{"a":"ü"}', 'utf8'));
+ });
+
+ it('throws for strings that do not contain valid JSON', function() {
+ assert.throws(() => {
+ TYPES.JSON.validate('{oops', undefined);
+ }, SyntaxError);
+ });
+
+ it('serializes objects and arrays to their UTF-8 encoded JSON representation', function() {
+ assert.deepEqual(TYPES.JSON.validate({ a: [1, 'ü'] }, undefined), Buffer.from('{"a":[1,"ü"]}', 'utf8'));
+ assert.deepEqual(TYPES.JSON.validate([1, 2], undefined), Buffer.from('[1,2]', 'utf8'));
+ });
+
+ it('throws for Buffer values', function() {
+ assert.throws(() => {
+ TYPES.JSON.validate(Buffer.from('{"a":1}'), undefined);
+ }, TypeError, 'Invalid JSON value.');
+ });
+
+ it('throws for values that can not be serialized to JSON', function() {
+ assert.throws(() => {
+ TYPES.JSON.validate(() => {}, undefined);
+ }, TypeError, 'Invalid JSON value.');
+ });
+ });
+});
diff --git a/test/unit/login7-payload-test.ts b/test/unit/login7-payload-test.ts
index fcff768c4..8d37cb3d2 100644
--- a/test/unit/login7-payload-test.ts
+++ b/test/unit/login7-payload-test.ts
@@ -175,7 +175,7 @@ describe('Login7Payload', function() {
2 + 2 + (2 * payload.changePassword.length) +
4 + // cbSSPILong
4 + // Extension offset
- 1 + (1 + 4 + 1) + (1 + 4 + 1) + 1; // Feature ext - v7.4 includes UTF8_SUPPORT unlike prior versions
+ 1 + (1 + 4 + 1) + (1 + 4 + 1) + (1 + 4 + 1) + 1; // Feature ext - v7.4 includes UTF8_SUPPORT and JSON_SUPPORT unlike prior versions
assert.lengthOf(data, expectedLength);
});
@@ -230,6 +230,7 @@ describe('Login7Payload', function() {
4 + // Extension offset
(1 + 4 + 1 + 4 + (token.length * 2)) + // SECURITYTOKEN feature
(1 + 4 + 1) + // UTF8_SUPPORT feature
+ (1 + 4 + 1) + // JSON_SUPPORT feature
1; // Terminator
assert.lengthOf(data, expectedLength);
@@ -282,7 +283,7 @@ describe('Login7Payload', function() {
2 + 2 + (2 * payload.changePassword.length) +
4 + // cbSSPILong
4 + // Extension offset
- 1 + (1 + 4 + 1) + (1 + 4 + 1) + 1; // Feature ext - v7.4 includes UTF8_SUPPORT unlike prior versions
+ 1 + (1 + 4 + 1) + (1 + 4 + 1) + (1 + 4 + 1) + 1; // Feature ext - v7.4 includes UTF8_SUPPORT and JSON_SUPPORT unlike prior versions
assert.lengthOf(data, expectedLength);
});
diff --git a/test/unit/token/colmetadata-token-parser-test.ts b/test/unit/token/colmetadata-token-parser-test.ts
index a45415136..161a348c4 100644
--- a/test/unit/token/colmetadata-token-parser-test.ts
+++ b/test/unit/token/colmetadata-token-parser-test.ts
@@ -122,4 +122,34 @@ describe('Colmetadata Token Parser', function() {
assert.strictEqual(token.columns[0].colName, 'name');
assert.strictEqual(token.columns[0].dataLength, length);
});
+
+ it('should parse json column metadata', async function() {
+ const debug = new Debug();
+ const numberOfColumns = 1;
+ const userType = 0;
+ const flags = 0;
+ const columnName = 'col1';
+
+ const buffer = new WritableTrackingBuffer(50, 'ucs2');
+
+ buffer.writeUInt8(0x81);
+ buffer.writeUInt16LE(numberOfColumns);
+ buffer.writeUInt32LE(userType);
+ buffer.writeUInt16LE(flags);
+ buffer.writeUInt8(typeByName.JSON.id);
+ buffer.writeBVarchar(columnName);
+
+ const parser = StreamParser.parseTokens([buffer.data], debug, options);
+
+ const result = await parser.next();
+ assert.isFalse(result.done);
+ const token = result.value;
+
+ assert.instanceOf(token, ColMetadataToken);
+ assert.strictEqual(token.columns.length, 1);
+ assert.strictEqual(token.columns[0].type.name, 'JSON');
+ assert.strictEqual(token.columns[0].colName, 'col1');
+ assert.isUndefined(token.columns[0].collation);
+ assert.isUndefined(token.columns[0].dataLength);
+ });
});
diff --git a/test/unit/token/feature-ext-parser-test.ts b/test/unit/token/feature-ext-parser-test.ts
index d63e60c6d..b61ffe924 100644
--- a/test/unit/token/feature-ext-parser-test.ts
+++ b/test/unit/token/feature-ext-parser-test.ts
@@ -58,6 +58,53 @@ describe('Feature Ext Parser', () => {
assert.instanceOf(token, FeatureExtAckToken);
assert.strictEqual(token.utf8Support, true); // feature ext ack for UTF8_SUPPORT was positive
assert.isUndefined(token.fedAuth); // fed auth not ack'd
+ assert.isUndefined(token.jsonSupport); // feature ext ack for JSON_SUPPORT was not received
+
+ assert.isTrue((await parser.next()).done);
+ });
+
+ it('should parse JSON support token', async function() {
+ const debug = new Debug();
+ const buffer = new WritableTrackingBuffer(8);
+
+ buffer.writeUInt8(0xAE); // FEATUREEXTACK token header
+ buffer.writeUInt8(0x0D); // JSON_SUPPORT feature id
+ buffer.writeUInt32LE(0x00_00_00_01); // datalen
+ buffer.writeUInt8(0x01); // version 1
+
+ buffer.writeUInt8(0xFF); // TERMINATOR
+
+ const parser = StreamParser.parseTokens([buffer.data], debug, options);
+ const result = await parser.next();
+ assert.isFalse(result.done);
+ const token = result.value;
+
+ assert.instanceOf(token, FeatureExtAckToken);
+ assert.deepEqual(token.jsonSupport, Buffer.from([0x01])); // raw JSON_SUPPORT acknowledgement data
+ assert.isUndefined(token.utf8Support); // feature ext ack for UTF8_SUPPORT was not received
+ assert.isUndefined(token.fedAuth); // fed auth not ack'd
+
+ assert.isTrue((await parser.next()).done);
+ });
+
+ it('should preserve the raw data of JSON support tokens with unknown versions', async function() {
+ const debug = new Debug();
+ const buffer = new WritableTrackingBuffer(8);
+
+ buffer.writeUInt8(0xAE); // FEATUREEXTACK token header
+ buffer.writeUInt8(0x0D); // JSON_SUPPORT feature id
+ buffer.writeUInt32LE(0x00_00_00_01); // datalen
+ buffer.writeUInt8(0x02); // version 2
+
+ buffer.writeUInt8(0xFF); // TERMINATOR
+
+ const parser = StreamParser.parseTokens([buffer.data], debug, options);
+ const result = await parser.next();
+ assert.isFalse(result.done);
+ const token = result.value;
+
+ assert.instanceOf(token, FeatureExtAckToken);
+ assert.deepEqual(token.jsonSupport, Buffer.from([0x02]));
assert.isTrue((await parser.next()).done);
});
diff --git a/test/unit/token/handler-test.ts b/test/unit/token/handler-test.ts
new file mode 100644
index 000000000..fece417b1
--- /dev/null
+++ b/test/unit/token/handler-test.ts
@@ -0,0 +1,61 @@
+import { assert } from 'chai';
+
+import { Login7TokenHandler } from '../../../src/token/handler';
+import { FeatureExtAckToken } from '../../../src/token/token';
+import { ConnectionError } from '../../../src/errors';
+import type Connection from '../../../src/connection';
+
+function buildConnection(): Connection {
+ return {
+ config: { authentication: { type: 'default', options: {} } },
+ serverSupportsJson: false
+ } as unknown as Connection;
+}
+
+describe('Login7TokenHandler', function() {
+ describe('#onFeatureExtAck', function() {
+ it('marks the server as json capable for a version 1 JSON support acknowledgement', function() {
+ const connection = buildConnection();
+ const handler = new Login7TokenHandler(connection);
+
+ handler.onFeatureExtAck(new FeatureExtAckToken(undefined, undefined, Buffer.from([0x01])));
+
+ assert.isTrue(connection.serverSupportsJson);
+ assert.isUndefined(handler.loginError);
+ });
+
+ it('leaves the server marked as not json capable without a JSON support acknowledgement', function() {
+ const connection = buildConnection();
+ const handler = new Login7TokenHandler(connection);
+
+ handler.onFeatureExtAck(new FeatureExtAckToken(undefined, true, undefined));
+
+ assert.isFalse(connection.serverSupportsJson);
+ assert.isUndefined(handler.loginError);
+ });
+
+ it('fails the login for a JSON support acknowledgement with an unknown version', function() {
+ const connection = buildConnection();
+ const handler = new Login7TokenHandler(connection);
+
+ handler.onFeatureExtAck(new FeatureExtAckToken(undefined, undefined, Buffer.from([0x02])));
+
+ assert.isFalse(connection.serverSupportsJson);
+ assert.instanceOf(handler.loginError, ConnectionError);
+ assert.strictEqual(handler.loginError!.message, 'Received invalid JSON support acknowledgement');
+ });
+
+ it('fails the login for a JSON support acknowledgement with invalid data', function() {
+ for (const data of [Buffer.alloc(0), Buffer.from([0x01, 0x01])]) {
+ const connection = buildConnection();
+ const handler = new Login7TokenHandler(connection);
+
+ handler.onFeatureExtAck(new FeatureExtAckToken(undefined, undefined, data));
+
+ assert.isFalse(connection.serverSupportsJson);
+ assert.instanceOf(handler.loginError, ConnectionError);
+ assert.strictEqual(handler.loginError!.message, 'Received invalid JSON support acknowledgement');
+ }
+ });
+ });
+});
diff --git a/test/unit/token/nbcrow-token-parser-test.ts b/test/unit/token/nbcrow-token-parser-test.ts
index 64b6796ce..5a71a7496 100644
--- a/test/unit/token/nbcrow-token-parser-test.ts
+++ b/test/unit/token/nbcrow-token-parser-test.ts
@@ -56,4 +56,41 @@ describe('NBCRow Token Parser', function() {
assert.isTrue((await parser.next()).done);
});
});
+
+ it('should parse json', 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.JSON,
+ collation: undefined
+ }];
+ const value = '{"a":"\u00fc"}';
+ const payload = Buffer.from(value, 'utf8');
+
+ const buffer = new WritableTrackingBuffer(0);
+ buffer.writeUInt8(0xd2);
+ buffer.writeUInt8(0x00); // null bitmap - column is not null
+ buffer.writeUInt64LE(payload.length); // PLP total length
+ buffer.writeUInt32LE(payload.length); // chunk length
+ buffer.writeBuffer(payload);
+ buffer.writeUInt32LE(0); // PLP terminator
+
+ const parser = Parser.parseTokens([buffer.data], debug, options, colMetadata);
+ const result = await parser.next();
+ assert.isFalse(result.done);
+ const token = result.value;
+
+ assert.instanceOf(token, NBCRowToken);
+ assert.strictEqual(token.columns.length, 1);
+ assert.strictEqual(token.columns[0].value, value);
+ assert.strictEqual(token.columns[0].metadata, colMetadata[0]);
+ assert.isTrue((await parser.next()).done);
+ });
});
diff --git a/test/unit/token/returnvalue-token-parser-test.ts b/test/unit/token/returnvalue-token-parser-test.ts
new file mode 100644
index 000000000..195a1a3b8
--- /dev/null
+++ b/test/unit/token/returnvalue-token-parser-test.ts
@@ -0,0 +1,70 @@
+import { assert } from 'chai';
+
+import Parser, { type ParserOptions } from '../../../src/token/stream-parser';
+import { ReturnValueToken } from '../../../src/token/token';
+import WritableTrackingBuffer from '../../../src/tracking-buffer/writable-tracking-buffer';
+import Debug from '../../../src/debug';
+
+const options = {
+ useUTC: false,
+ tdsVersion: '7_2'
+} as ParserOptions;
+
+describe('ReturnValue Token Parser', function() {
+ it('should parse json values', async function() {
+ const debug = new Debug();
+ const value = '{"a":"ü"}';
+ const payload = Buffer.from(value, 'utf8');
+
+ const buffer = new WritableTrackingBuffer(0, 'ucs2');
+ buffer.writeUInt8(0xAC); // RETURNVALUE token
+ buffer.writeUInt16LE(1); // paramOrdinal
+ buffer.writeBVarchar('@out'); // paramName
+ buffer.writeUInt8(0x01); // status
+ buffer.writeUInt32LE(0); // userType
+ buffer.writeUInt16LE(0); // flags
+ buffer.writeUInt8(0xF4); // json TYPE_INFO
+ buffer.writeUInt64LE(payload.length); // PLP total length
+ buffer.writeUInt32LE(payload.length); // chunk length
+ buffer.writeBuffer(payload);
+ buffer.writeUInt32LE(0); // PLP terminator
+
+ const parser = Parser.parseTokens([buffer.data], debug, options);
+ const result = await parser.next();
+ assert.isFalse(result.done);
+ const token = result.value;
+
+ assert.instanceOf(token, ReturnValueToken);
+ assert.strictEqual(token.paramOrdinal, 1);
+ assert.strictEqual(token.paramName, 'out');
+ assert.strictEqual(token.metadata.type.name, 'JSON');
+ assert.strictEqual(token.value, value);
+ assert.isTrue((await parser.next()).done);
+ });
+
+ it('should parse null json values', async function() {
+ const debug = new Debug();
+
+ const buffer = new WritableTrackingBuffer(0, 'ucs2');
+ buffer.writeUInt8(0xAC); // RETURNVALUE token
+ buffer.writeUInt16LE(1); // paramOrdinal
+ buffer.writeBVarchar('@out'); // paramName
+ buffer.writeUInt8(0x01); // status
+ buffer.writeUInt32LE(0); // userType
+ buffer.writeUInt16LE(0); // flags
+ buffer.writeUInt8(0xF4); // json TYPE_INFO
+ buffer.writeBuffer(Buffer.alloc(8, 0xFF)); // PLP null
+
+ const parser = Parser.parseTokens([buffer.data], debug, options);
+ const result = await parser.next();
+ assert.isFalse(result.done);
+ const token = result.value;
+
+ assert.instanceOf(token, ReturnValueToken);
+ assert.strictEqual(token.paramOrdinal, 1);
+ assert.strictEqual(token.paramName, 'out');
+ assert.strictEqual(token.metadata.type.name, 'JSON');
+ assert.isNull(token.value);
+ assert.isTrue((await parser.next()).done);
+ });
+});
diff --git a/test/unit/token/row-token-parser-test.ts b/test/unit/token/row-token-parser-test.ts
index f45c0e4b3..c4d9fd631 100644
--- a/test/unit/token/row-token-parser-test.ts
+++ b/test/unit/token/row-token-parser-test.ts
@@ -1427,4 +1427,70 @@ describe('Row Token Parser', function() {
assert.strictEqual(token.columns[0].value, null);
assert.isTrue((await parser.next()).done);
});
+
+ it('should parse json', 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.JSON,
+ collation: undefined
+ }];
+ const value = '{"a":"\u00fc"}';
+ const payload = Buffer.from(value, 'utf8');
+
+ const buffer = new WritableTrackingBuffer(0);
+ buffer.writeUInt8(0xd1);
+ buffer.writeUInt64LE(payload.length); // PLP total length
+ buffer.writeUInt32LE(payload.length); // chunk length
+ buffer.writeBuffer(payload);
+ buffer.writeUInt32LE(0); // PLP terminator
+
+ const parser = Parser.parseTokens([buffer.data], 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, 1);
+ assert.strictEqual(token.columns[0].value, value);
+ assert.strictEqual(token.columns[0].metadata, colMetadata[0]);
+ assert.isTrue((await parser.next()).done);
+ });
+
+ it('should parse null json', 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.JSON,
+ collation: undefined
+ }];
+
+ const buffer = new WritableTrackingBuffer(0);
+ buffer.writeUInt8(0xd1);
+ buffer.writeBuffer(Buffer.alloc(8, 0xFF)); // PLP null
+
+ const parser = Parser.parseTokens([buffer.data], 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, 1);
+ assert.strictEqual(token.columns[0].value, null);
+ assert.isTrue((await parser.next()).done);
+ });
});