diff --git a/handwritten/spanner/src/partial-result-stream.ts b/handwritten/spanner/src/partial-result-stream.ts index 18f492e577c..dde0c01de24 100644 --- a/handwritten/spanner/src/partial-result-stream.ts +++ b/handwritten/spanner/src/partial-result-stream.ts @@ -121,6 +121,22 @@ export interface Row extends Array { toJSON(options?: JSONOptions): Json; } +/** + * Row implementation extending Array to provide a shared, non-enumerable + * toJSON method without per-row closures or Object.setPrototypeOf overhead. + */ +class RowImpl extends Array implements Row { + toJSON(options?: JSONOptions): Json { + return codec.convertFieldsToJson(this, options); + } +} +Object.defineProperty(RowImpl.prototype, 'constructor', { + value: Array, + writable: true, + configurable: true, + enumerable: false, +}); + /** * @callback PartialResultStream~rowCallback * @param {Row|object} row The row data. @@ -458,7 +474,7 @@ export class PartialResultStream extends Transform implements ResultEvents { */ private _createRow(values: Value[]): Row { const len = values.length; - const fields = new Array(len); + const fields = new RowImpl(len); const decoders = this._decoders; const classFields = this._fields; @@ -469,13 +485,7 @@ export class PartialResultStream extends Transform implements ResultEvents { }; } - Object.defineProperty(fields, 'toJSON', { - value: (options?: JSONOptions): Json => { - return codec.convertFieldsToJson(fields, options); - }, - }); - - return fields as Row; + return fields; } /** * Attempts to merge chunked values together. diff --git a/handwritten/spanner/test/partial-result-stream.ts b/handwritten/spanner/test/partial-result-stream.ts index 1aafbfca6a8..f1a0169e51b 100644 --- a/handwritten/spanner/test/partial-result-stream.ts +++ b/handwritten/spanner/test/partial-result-stream.ts @@ -200,6 +200,45 @@ describe('PartialResultStream', () => { stream.write(RESULT); }); + it('should create rows with shared prototype and non-enumerable toJSON', done => { + const rows: prs.Row[] = []; + stream.on('error', done).on('data', row => { + rows.push(row); + if (rows.length === 2) { + try { + const [row1, row2] = rows; + assert.strictEqual(Array.isArray(row1), true); + assert.strictEqual(row1 instanceof Array, true); + assert.strictEqual(row1.constructor, Array); + assert.strictEqual(Array.isArray(row2), true); + assert.strictEqual(row2 instanceof Array, true); + assert.strictEqual(row2.constructor, Array); + + // toJSON must be non-enumerable + assert.strictEqual(Object.keys(row1).includes('toJSON'), false); + assert.strictEqual( + Object.prototype.propertyIsEnumerable.call(row1, 'toJSON'), + false, + ); + + // toJSON must be shared on the prototype, not created as a per-row closure + assert.strictEqual(row1.toJSON, row2.toJSON); + + // toJSON should correctly serialize the row + const json1 = row1.toJSON(); + const expectedJson = codec.convertFieldsToJson(row1); + assert.deepStrictEqual(json1, expectedJson); + done(); + } catch (error) { + done(error); + } + } + }); + + stream.write(RESULT); + stream.write({values: [convertToIValue(VALUE)]}); + }); + it('should emit rows as JSON', done => { const jsonOptions = {}; const stream = new PartialResultStream({json: true, jsonOptions});