From 9a34768aef0906eeb78ccf8c56f355c54d165502 Mon Sep 17 00:00:00 2001 From: Rhys Howell Date: Tue, 18 Aug 2026 09:51:06 -0400 Subject: [PATCH 1/3] fix(query-parser): update DBRef stringify to preserve type information COMPASS-10987 --- packages/query-parser/src/index.spec.ts | 66 +++++++++++++++++++++++-- packages/query-parser/src/stringify.ts | 8 +-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/packages/query-parser/src/index.spec.ts b/packages/query-parser/src/index.spec.ts index 676b2158..78c86d6e 100644 --- a/packages/query-parser/src/index.spec.ts +++ b/packages/query-parser/src/index.spec.ts @@ -444,6 +444,66 @@ e s`, a: new bson.Code(code, { b: 1 }), }); }); + + describe('DBRef', function () { + // The bson types say `oid` is an ObjectId, but a DBRef oid can hold any + // BSON value in practice, which is what we want to cover here. + const dbRef = (collection: string, oid: unknown, db?: string) => + new bson.DBRef(collection, oid as bson.ObjectId, db); + + it('preserves the oid type rather than flattening it to a string', function () { + assert.equal( + toJSString({ a: dbRef('col', 1) }, 0), + '{a:DBRef("col", 1)}', + ); + assert.equal( + toJSString({ a: dbRef('col', 'abc') }, 0), + '{a:DBRef("col", \'abc\')}', + ); + assert.equal( + toJSString( + { a: dbRef('col', new bson.ObjectId('507f191e810c19729de860ea')) }, + 0, + ), + '{a:DBRef("col", ObjectId(\'507f191e810c19729de860ea\'))}', + ); + }); + + it('includes the db when present', function () { + assert.equal( + toJSString({ a: dbRef('col', 1, 'db') }, 0), + '{a:DBRef("col", 1, "db")}', + ); + }); + + it('escapes quotes in the collection and db', function () { + assert.equal( + toJSString({ a: dbRef("co'l", 1, 'd"b') }, 0), + '{a:DBRef("co\'l", 1, "d\\"b")}', + ); + }); + + const roundTrips: [string, bson.DBRef][] = [ + ['numeric oid', dbRef('col', 1)], + ['string oid', dbRef('col', 'abc')], + [ + 'ObjectId oid', + dbRef('col', new bson.ObjectId('507f191e810c19729de860ea')), + ], + ['db', dbRef('col', 1, 'db')], + ['quotes', dbRef("co'l", 1, 'd"b')], + ['double spaces', dbRef('a b', 1)], + ['newline', dbRef('a\nb', 1)], + ['nested DBRef oid', dbRef('col', dbRef('inner', 1), 'db')], + ]; + + for (const [name, dbref] of roundTrips) { + it(`round-trips a DBRef with ${name}`, function () { + const jsString = toJSString({ a: dbref }, 0) as string; + assert.deepEqual(parseFilter(jsString), { a: dbref }); + }); + } + }); }); describe('stringify', function () { @@ -566,7 +626,7 @@ e s`, it('correctly converts to a DBRef', function () { const res = parseFilter("{dbref: DBRef('col', 1)}"); const stringified = stringify(res); - assert.equal(stringified, "{dbref: DBRef('col', '1')}"); + assert.equal(stringified, '{dbref: DBRef("col", 1)}'); }); }); @@ -574,7 +634,7 @@ e s`, it('correctly converts to a DBRef', function () { const res = parseFilter("{dbref: DBRef('db.col', 1)}"); const stringified = stringify(res); - assert.equal(stringified, "{dbref: DBRef('col', '1', 'db')}"); + assert.equal(stringified, '{dbref: DBRef("col", 1, "db")}'); }); }); @@ -582,7 +642,7 @@ e s`, it('correctly converts to a DBRef', function () { const res = parseFilter("{dbref: DBRef('col', 1, 'db')}"); const stringified = stringify(res); - assert.equal(stringified, "{dbref: DBRef('col', '1', 'db')}"); + assert.equal(stringified, '{dbref: DBRef("col", 1, "db")}'); }); }); diff --git a/packages/query-parser/src/stringify.ts b/packages/query-parser/src/stringify.ts index 4014e7ab..b93ff0c7 100644 --- a/packages/query-parser/src/stringify.ts +++ b/packages/query-parser/src/stringify.ts @@ -80,11 +80,13 @@ const BSON_TO_JS_STRING = { return `BinData(${subType.toString(10)}, '${v.toString('base64')}')`; }, DBRef: function (v: DBRef) { + // `toJSString` only returns undefined for values that stringify to + // nothing, which for an oid can only be `undefined` itself. + const oid = toJSString(v.oid, 0) ?? 'undefined'; if (v.db) { - return `DBRef('${v.collection}', '${v.oid.toString()}', '${v.db}')`; + return `DBRef(${JSON.stringify(v.collection)}, ${oid}, ${JSON.stringify(v.db)})`; } - - return `DBRef('${v.collection}', '${v.oid.toString()}')`; + return `DBRef(${JSON.stringify(v.collection)}, ${oid})`; }, Timestamp: function (v: Timestamp) { return `Timestamp({ t: ${v.high}, i: ${v.low} })`; From 10b78275e412ed643f68e81d0946e36657d0b0c2 Mon Sep 17 00:00:00 2001 From: Rhys Howell Date: Tue, 18 Aug 2026 16:49:37 -0400 Subject: [PATCH 2/3] fixup: render fields --- packages/query-parser/src/index.spec.ts | 51 ++++++++++++++++++++++++- packages/query-parser/src/stringify.ts | 14 +++++-- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/query-parser/src/index.spec.ts b/packages/query-parser/src/index.spec.ts index 78c86d6e..1a2210c7 100644 --- a/packages/query-parser/src/index.spec.ts +++ b/packages/query-parser/src/index.spec.ts @@ -448,8 +448,12 @@ e s`, describe('DBRef', function () { // The bson types say `oid` is an ObjectId, but a DBRef oid can hold any // BSON value in practice, which is what we want to cover here. - const dbRef = (collection: string, oid: unknown, db?: string) => - new bson.DBRef(collection, oid as bson.ObjectId, db); + const dbRef = ( + collection: string, + oid: unknown, + db?: string, + fields?: Record, + ) => new bson.DBRef(collection, oid as bson.ObjectId, db, fields); it('preserves the oid type rather than flattening it to a string', function () { assert.equal( @@ -476,6 +480,41 @@ e s`, ); }); + it('includes the fields when present', function () { + assert.equal( + toJSString({ a: dbRef('col', 1, 'db', { b: 1 }) }, 0), + '{a:DBRef("col", 1, "db", {b:1})}', + ); + }); + + it('passes an undefined db when fields are present without one', function () { + assert.equal( + toJSString({ a: dbRef('col', 1, undefined, { b: 1 }) }, 0), + '{a:DBRef("col", 1, undefined, {b:1})}', + ); + }); + + it('omits empty fields', function () { + assert.equal( + toJSString({ a: dbRef('col', 1, undefined, {}) }, 0), + '{a:DBRef("col", 1)}', + ); + }); + + it('preserves BSON types inside fields', function () { + assert.equal( + toJSString( + { + a: dbRef('col', 1, 'db', { + b: new bson.ObjectId('507f191e810c19729de860ea'), + }), + }, + 0, + ), + '{a:DBRef("col", 1, "db", {b:ObjectId(\'507f191e810c19729de860ea\')})}', + ); + }); + it('escapes quotes in the collection and db', function () { assert.equal( toJSString({ a: dbRef("co'l", 1, 'd"b') }, 0), @@ -495,6 +534,14 @@ e s`, ['double spaces', dbRef('a b', 1)], ['newline', dbRef('a\nb', 1)], ['nested DBRef oid', dbRef('col', dbRef('inner', 1), 'db')], + ['fields', dbRef('col', 1, 'db', { b: 1 })], + ['fields but no db', dbRef('col', 1, undefined, { b: 1 })], + [ + 'BSON values in fields', + dbRef('col', 1, 'db', { + b: new bson.ObjectId('507f191e810c19729de860ea'), + }), + ], ]; for (const [name, dbref] of roundTrips) { diff --git a/packages/query-parser/src/stringify.ts b/packages/query-parser/src/stringify.ts index b93ff0c7..48a2b7ed 100644 --- a/packages/query-parser/src/stringify.ts +++ b/packages/query-parser/src/stringify.ts @@ -83,10 +83,18 @@ const BSON_TO_JS_STRING = { // `toJSString` only returns undefined for values that stringify to // nothing, which for an oid can only be `undefined` itself. const oid = toJSString(v.oid, 0) ?? 'undefined'; - if (v.db) { - return `DBRef(${JSON.stringify(v.collection)}, ${oid}, ${JSON.stringify(v.db)})`; + const args = [JSON.stringify(v.collection), oid]; + // `fields` defaults to an empty object rather than being unset, so only + // include it when it actually holds something. + const hasFields = !!v.fields && Object.keys(v.fields).length > 0; + if (v.db || hasFields) { + // `db` has to be present for `fields` to land in the right position. + args.push(v.db === undefined ? 'undefined' : JSON.stringify(v.db)); } - return `DBRef(${JSON.stringify(v.collection)}, ${oid})`; + if (hasFields) { + args.push(toJSString(v.fields, 0) ?? '{}'); + } + return `DBRef(${args.join(', ')})`; }, Timestamp: function (v: Timestamp) { return `Timestamp({ t: ${v.high}, i: ${v.low} })`; From 194767c1a553a683dd4de425d647e99aa62e1afd Mon Sep 17 00:00:00 2001 From: Rhys Howell Date: Tue, 18 Aug 2026 17:38:20 -0400 Subject: [PATCH 3/3] fixup: decrease date precision, one was 6 ms off --- packages/shell-bson-parser/src/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shell-bson-parser/src/index.spec.ts b/packages/shell-bson-parser/src/index.spec.ts index 20da8f1a..a1f6f959 100644 --- a/packages/shell-bson-parser/src/index.spec.ts +++ b/packages/shell-bson-parser/src/index.spec.ts @@ -531,7 +531,7 @@ describe('@mongodb-js/shell-bson-parser', function () { // When constructing a date with no arguments, it will be set to the current date, // which is prone to race conditions for millisecond precision. - const allowedMillisecondDelta = args.length === 0 ? 3 : 0; + const allowedMillisecondDelta = args.length === 0 ? 9 : 0; expect(actual.getDate).to.equal( new (Date as any)(...args).getDate(),