SERVER-107499 Implement BSON to Extended JSON (in BSON) conversion (#44519)

GitOrigin-RevId: 9f51ee9e3f7f8dc8e7003232248380093ec64b64
This commit is contained in:
Vesko Karaganev 2025-12-09 15:22:04 +00:00 committed by MongoDB Bot
parent 839610ff63
commit 2418787761
7 changed files with 658 additions and 0 deletions

1
.github/CODEOWNERS vendored
View File

@ -2022,6 +2022,7 @@ WORKSPACE.bazel @10gen/devprod-build @svc-auto-approve-bot
/src/mongo/db/exec/**/trial_run_tracker.h @10gen/query-optimization @svc-auto-approve-bot
/src/mongo/db/exec/**/trial_stage* @10gen/query-optimization @svc-auto-approve-bot
/src/mongo/db/exec/**/scoped_timer* @10gen/server-programmability @svc-auto-approve-bot
/src/mongo/db/exec/**/serialize_ejson* @10gen/query-optimization @svc-auto-approve-bot
# The following patterns are parsed from ./src/mongo/db/exec/agg/OWNERS.yml
/src/mongo/db/exec/agg/**/* @10gen/query-execution-classic @svc-auto-approve-bot

View File

@ -1762,6 +1762,7 @@ mongo_cc_library(
"//src/mongo/crypto:fle_tokens",
"//src/mongo/db/commands:test_commands_enabled",
"//src/mongo/db/exec:convert_utils",
"//src/mongo/db/exec:serialize_ejson_utils",
"//src/mongo/db/exec:str_trim_utils",
"//src/mongo/db/exec:substr_utils",
"//src/mongo/db/exec/document_value",

View File

@ -132,6 +132,7 @@ mongo_cc_unit_test(
"projection_executor_test.cpp",
"projection_executor_utils_test.cpp",
"projection_executor_wildcard_access_test.cpp",
"serialize_ejson_utils_test.cpp",
"//src/mongo/db/exec/agg:exec_pipeline_test.cpp",
"//src/mongo/db/exec/agg:pipeline_builder_test.cpp",
"//src/mongo/db/exec/classic:distinct_scan_test.cpp",
@ -183,6 +184,7 @@ mongo_cc_unit_test(
tags = ["mongo_unittest_fourth_group"],
deps = [
":projection_executor",
":serialize_ejson_utils",
":working_set",
"//src/mongo:base",
"//src/mongo/bson/column",
@ -275,3 +277,14 @@ mongo_cc_fuzzer_test(
"//src/mongo/transport:transport_layer_common",
],
)
mongo_cc_library(
name = "serialize_ejson_utils",
srcs = [
"serialize_ejson_utils.cpp",
],
deps = [
"//src/mongo:base",
"//src/mongo/db/exec/document_value",
],
)

View File

@ -41,3 +41,6 @@ filters:
- "scoped_timer*":
approvers:
- 10gen/server-programmability
- "serialize_ejson*":
approvers:
- 10gen/query-optimization

View File

@ -0,0 +1,309 @@
/**
* Copyright (C) 2025-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the Server Side Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include "mongo/db/exec/serialize_ejson_utils.h"
#include "mongo/bson/bson_depth.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/db/exec/document_value/document.h"
#include "mongo/db/exec/document_value/value.h"
#include "mongo/platform/decimal128.h"
#include "mongo/util/base64.h"
namespace mongo::exec::expression::serialize_ejson_utils {
namespace {
/**
* Assert the depth limit of the resulting value.
*/
void assertDepthLimit(const Value& value) {
auto maxDepth = BSONDepth::getMaxAllowableDepth();
uassert(ErrorCodes::ConversionFailure,
fmt::format("Result exceeds maximum depth limit of {} levels of nesting", maxDepth),
value.depth(maxDepth) != -1);
}
/**
* Assert that the value will be under the BSON size limit.
*/
void assertSizeLimit(const Value& value) {
// Overhead of [a] over a. We use a boolean placeholder and subtract it's size.
static const auto singleElementArrayOverhead = BSON_ARRAY(true).objsize() - 1;
try {
// We cannot validate the BSONObj size of a value unless we serialize it.
// To do this generically for non-objects, we can wrap in an array and validate against the
// adjusted size excluding the array overhead.
Document::validateDocumentBSONSize(BSON_ARRAY(value),
BSONObjMaxUserSize + singleElementArrayOverhead);
} catch (const ExceptionFor<ErrorCodes::BSONObjectTooLarge>&) {
uasserted(ErrorCodes::ConversionFailure,
fmt::format("Result exceeds maximum BSON size limit of {}", BSONObjMaxUserSize));
}
}
/**
* Options controlling the behaviour of the Extended JSON conversion.
*/
struct ExtendedJsonOptions {
/// Enables the Extended JSON v2 Relaxed representation.
bool relaxed{true};
/// Enables local time ISO8601 formatting for $date (relaxed format only).
bool localDate{dateFormatIsLocalTimezone()};
};
/// Formats according to the Extended JSON spec for $uuid.
std::string uuidToFormattedString(StringData data) {
return UUID::fromCDR(data).toString();
}
// Define string constants to avoid misspelling :)
constexpr StringData kMinKey = "$minKey";
constexpr StringData kMaxKey = "$maxKey";
constexpr StringData kUndefined = "$undefined";
constexpr StringData kNumberInt = "$numberInt";
constexpr StringData kNumberLong = "$numberLong";
constexpr StringData kNumberDouble = "$numberDouble";
constexpr StringData kNumberDecimal = "$numberDecimal";
constexpr StringData kBinary = "$binary";
constexpr StringData kUuid = "$uuid";
constexpr StringData kSubType = "subType";
constexpr StringData kBase64 = "base64";
constexpr StringData kOid = "$oid";
constexpr StringData kDate = "$date";
constexpr StringData kRegularExpression = "$regularExpression";
constexpr StringData kPattern = "pattern";
constexpr StringData kOptions = "options";
constexpr StringData kDbPointer = "$dbPointer";
constexpr StringData kRef = "$ref";
constexpr StringData kId = "$id";
constexpr StringData kCode = "$code";
constexpr StringData kSymbol = "$symbol";
constexpr StringData kNaN = "NaN";
constexpr StringData kPosInfinity = "Infinity";
constexpr StringData kNegInfinity = "-Infinity";
constexpr StringData kScope = "$scope";
constexpr StringData kTimestamp = "$timestamp";
/**
* Callable which knows how to convert every BSON type to Extended JSON.
* The format options are passed at construction.
*/
struct ToExtendedJsonConverter {
Value object(const Document& doc) const {
MutableDocument newDoc;
for (auto it = doc.fieldIterator(); it.more();) {
auto p = it.next();
newDoc.setField(p.first, (*this)(p.second));
}
return newDoc.freezeToValue();
}
Value array(const std::vector<Value>& arr) const {
std::vector<Value> newArr;
newArr.reserve(arr.size());
for (auto&& v : arr) {
newArr.emplace_back((*this)(v));
}
return Value(std::move(newArr));
}
Value binData(const BSONBinData& binData) const {
StringData data(static_cast<const char*>(binData.data), binData.length);
if (binData.type == BinDataType::newUUID && binData.length == UUID::kNumBytes) {
// We are permitted to but not required to emit $uuid under the spec.
// However ExtendedCanonicalV200Generator does this, so we do the same here.
// This may be expected by users and it also has a benefit for us - it allows us to test
// for equivalence between ExtendedCanonicalV200Generator and this implementation more
// easily.
return Value(BSON(kUuid << uuidToFormattedString(data)));
}
fmt::memory_buffer buffer;
base64::encode(buffer, data);
return Value(
BSON(kBinary << BSON(kBase64 << StringData(buffer.data(), buffer.size()) << kSubType
<< fmt::format("{:x}", binData.type))));
}
Value oid(const OID& oid) const {
static_assert(OID::kOIDSize == 12);
return Value(BSON(kOid << oid.toString()));
}
Value date(Date_t date) const {
if (opts.relaxed && date.isFormattable()) {
return Value(
BSON(kDate << StringData{DateStringBuffer{}.iso8601(date, opts.localDate)}));
}
return Value(BSON(kDate << BSON(kNumberLong << fmt::to_string(date.toMillisSinceEpoch()))));
}
Value regEx(const char* pattern, const char* options) const {
return Value(BSON(kRegularExpression << BSON(kPattern << pattern << kOptions << options)));
}
Value dbRef(const BSONDBRef& dbRef) const {
// ExtendedCanonicalV200Generator seems to generate the wrong representation here.
// Our BSONType::dbRef maps to dbPointer (typeName(BSONType::dbRef) == "dbPointer").
// There are two types named on the spec page: dbRef ("convention" - not native type)
// and dbPointer (native type). dbPointer (BSONType::dbRef) should use a $dbPointer
// wrapper, but our ExtendedCanonicalV200Generator follows the rules set out for the
// dbRef convention, not the native type.
return Value(BSON(kDbPointer << BSON(kRef << dbRef.ns << kId << dbRef.oid.toString())));
}
Value code(const std::string& code) const {
return Value(BSON(kCode << code));
}
Value symbol(const std::string& symbol) const {
return Value(BSON(kSymbol << symbol));
}
Value codeWScope(const BSONCodeWScope& cws) const {
// The $scope always uses canonical format.
ToExtendedJsonConverter scopeConverter = *this;
scopeConverter.opts.relaxed = false;
return Value(
BSON(kCode << cws.code << kScope << scopeConverter.object(Document(cws.scope))));
}
Value timestamp(Timestamp ts) const {
return Value(BSON(kTimestamp << BSON("t" << static_cast<long long>(ts.getSecs()) << "i"
<< static_cast<long long>(ts.getInc()))));
}
Value numberInt(int num) const {
if (opts.relaxed) {
return Value(num);
}
return Value(BSON(kNumberInt << fmt::to_string(num)));
}
Value numberLong(long long num) const {
if (opts.relaxed) {
return Value(num);
}
return Value(BSON(kNumberLong << fmt::to_string(num)));
}
Value numberDouble(double num) const {
if (std::isnan(num)) {
return Value(BSON(kNumberDouble << kNaN));
}
if (std::isinf(num)) {
if (num < 0) {
return Value(BSON(kNumberDouble << kNegInfinity));
} else {
return Value(BSON(kNumberDouble << kPosInfinity));
}
}
if (opts.relaxed) {
return Value(num);
}
return Value(BSON(kNumberDouble << fmt::to_string(num)));
}
Value numberDecimal(Decimal128 num) const {
if (num.isNaN()) {
return Value(BSON(kNumberDecimal << kNaN));
}
if (num.isInfinite()) {
if (num.isNegative()) {
return Value(BSON(kNumberDecimal << kNegInfinity));
} else {
return Value(BSON(kNumberDecimal << kPosInfinity));
}
}
return Value(BSON(kNumberDecimal << num.toString()));
}
Value operator()(const Value& value) const {
switch (value.getType()) {
case BSONType::minKey:
return Value(BSON(kMinKey << 1));
case BSONType::eoo:
uasserted(ErrorCodes::BadValue, "Unexpected eoo/missing value");
case BSONType::numberDouble:
return numberDouble(value.getDouble());
case BSONType::object:
return object(value.getDocument());
case BSONType::array:
return array(value.getArray());
case BSONType::binData:
return binData(value.getBinData());
case BSONType::undefined:
return Value(BSON(kUndefined << true));
case BSONType::oid:
return oid(value.getOid());
case BSONType::date:
return date(value.getDate());
case BSONType::regEx:
return regEx(value.getRegex(), value.getRegexFlags());
case BSONType::dbRef:
return dbRef(value.getDBRef());
case BSONType::code:
return code(value.getCode());
case BSONType::symbol:
return symbol(value.getSymbol());
case BSONType::codeWScope:
return codeWScope(value.getCodeWScope());
case BSONType::numberInt:
return numberInt(value.getInt());
case BSONType::timestamp:
return timestamp(value.getTimestamp());
case BSONType::numberLong:
return numberLong(value.getLong());
case BSONType::numberDecimal:
return numberDecimal(value.getDecimal());
case BSONType::maxKey:
return Value(BSON(kMaxKey << 1));
case BSONType::string:
case BSONType::boolean:
case BSONType::null:
return value;
}
MONGO_UNREACHABLE;
}
ExtendedJsonOptions opts;
};
} // namespace
Value serializeToExtendedJson(const Value& value, bool relaxed) {
auto result = ToExtendedJsonConverter({.relaxed = relaxed})(value);
assertDepthLimit(result);
assertSizeLimit(result);
return result;
}
} // namespace mongo::exec::expression::serialize_ejson_utils

View File

@ -0,0 +1,46 @@
/**
* Copyright (C) 2025-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the Server Side Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#pragma once
#include "mongo/db/exec/document_value/value.h"
#include "mongo/util/modules.h"
namespace mongo::exec::expression::serialize_ejson_utils {
/**
* Transform a BSON value to the equivalent Extended JSON, represented in BSON.
* The 'value' is mapped to Extended JSON following the Extended JSON v2 specification.
* The 'value' cannot be missing/BSONType::eoo. The parameter 'relaxed' selects between the Relaxed
* and Canonical specification.
* Note: In Relaxed mode, the types of numeric values are preserved in the result.
*/
Value serializeToExtendedJson(const Value& value, bool relaxed);
} // namespace mongo::exec::expression::serialize_ejson_utils

View File

@ -0,0 +1,285 @@
/**
* Copyright (C) 2025-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the Server Side Public License in all respects for
* all of the code used other than as permitted herein. If you modify file(s)
* with this exception, you may extend this exception to your version of the
* file(s), but you are not obligated to do so. If you do not wish to do so,
* delete this exception statement from your version. If you delete this
* exception statement from all source files in the program, then also delete
* it in the license file.
*/
#include "mongo/db/exec/serialize_ejson_utils.h"
#include "mongo/bson/bson_depth.h"
#include "mongo/db/exec/document_value/document_value_test_util.h"
#include "mongo/unittest/unittest.h"
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kTest
namespace mongo::exec::expression::serialize_ejson_utils {
namespace {
/// Specify if the input contains any BSON-only types or values.
enum class JsonCompatible { no, yes };
/**
* Holds input BSON and expected Extended JSON representations.
*/
struct TestCase {
explicit(false) TestCase(auto&& b, auto&& c, auto&& r, JsonCompatible j)
: bson(b), canonical(c), relaxed(r), jsonCompatible(j) {}
Value bson;
Value canonical;
Value relaxed;
JsonCompatible jsonCompatible;
};
constexpr uint8_t kUuidBytes[] = {0, 0, 0, 0, 0, 0, 0x40, 0, 0x80, 0, 0, 0, 0, 0, 0, 0};
const TestCase testCases[]{
// minKey
{MINKEY, BSON("$minKey" << 1), BSON("$minKey" << 1), JsonCompatible::no},
// double
{42.42, BSON("$numberDouble" << "42.42"), 42.42, JsonCompatible::yes},
{1e-18, BSON("$numberDouble" << "1e-18"), 1e-18, JsonCompatible::yes},
{std::numeric_limits<double>::infinity(),
BSON("$numberDouble" << "Infinity"),
BSON("$numberDouble" << "Infinity"),
JsonCompatible::no},
{-std::numeric_limits<double>::infinity(),
BSON("$numberDouble" << "-Infinity"),
BSON("$numberDouble" << "-Infinity"),
JsonCompatible::no},
{std::numeric_limits<double>::quiet_NaN(),
BSON("$numberDouble" << "NaN"),
BSON("$numberDouble" << "NaN"),
JsonCompatible::no},
// string
{"string"_sd, "string"_sd, "string"_sd, JsonCompatible::yes},
// object
{BSON("foo" << 1),
BSON("foo" << BSON("$numberInt" << "1")),
BSON("foo" << 1),
JsonCompatible::yes},
{BSON("foo" << BSON("bar" << 1)),
BSON("foo" << BSON("bar" << BSON("$numberInt" << "1"))),
BSON("foo" << BSON("bar" << 1)),
JsonCompatible::yes},
// array
{BSON_ARRAY(1 << 2),
BSON_ARRAY(BSON("$numberInt" << "1") << BSON("$numberInt" << "2")),
BSON_ARRAY(1 << 2),
JsonCompatible::yes},
// binData
{BSONBinData("123", 3, BinDataType::newUUID),
BSON("$binary" << BSON("base64" << "MTIz" << "subType" << "4")),
BSON("$binary" << BSON("base64" << "MTIz" << "subType" << "4")),
JsonCompatible::no},
{BSONBinData(kUuidBytes, 16, BinDataType::newUUID),
BSON("$uuid" << "00000000-0000-4000-8000-000000000000"),
BSON("$uuid" << "00000000-0000-4000-8000-000000000000"),
JsonCompatible::no},
{BSONBinData("123", 3, BinDataType::bdtCustom),
BSON("$binary" << BSON("base64" << "MTIz" << "subType" << "80")),
BSON("$binary" << BSON("base64" << "MTIz" << "subType" << "80")),
JsonCompatible::no},
// undefined
{BSONUndefined, BSON("$undefined" << true), BSON("$undefined" << true), JsonCompatible::no},
// oid
{OID("57e193d7a9cc81b4027498b5"),
BSON("$oid" << "57e193d7a9cc81b4027498b5"),
BSON("$oid" << "57e193d7a9cc81b4027498b5"),
JsonCompatible::no},
// boolean
{true, true, true, JsonCompatible::yes},
{false, false, false, JsonCompatible::yes},
// date
{Date_t(),
BSON("$date" << BSON("$numberLong" << "0")),
BSON("$date" << "1970-01-01T00:00:00.000Z"),
JsonCompatible::no},
{Date_t::max(),
BSON("$date" << BSON("$numberLong" << "9223372036854775807")),
BSON("$date" << BSON("$numberLong" << "9223372036854775807")),
JsonCompatible::no},
{Date_t::min(),
BSON("$date" << BSON("$numberLong" << "-9223372036854775808")),
BSON("$date" << BSON("$numberLong" << "-9223372036854775808")),
JsonCompatible::no},
{Date_t::fromDurationSinceEpoch(stdx::chrono::years{50}),
BSON("$date" << BSON("$numberLong" << "1577847600000")),
BSON("$date" << "2020-01-01T03:00:00.000Z"),
JsonCompatible::no},
// null
{BSONNULL, BSONNULL, BSONNULL, JsonCompatible::yes},
// regEx
{BSONRegEx("foo*", "ig"),
BSON("$regularExpression" << BSON("pattern" << "foo*" << "options" << "ig")),
BSON("$regularExpression" << BSON("pattern" << "foo*" << "options" << "ig")),
JsonCompatible::no},
// dbRef
{BSONDBRef("collection", OID("57e193d7a9cc81b4027498b1")),
BSON("$dbPointer" << BSON("$ref" << "collection" << "$id" << "57e193d7a9cc81b4027498b1")),
BSON("$dbPointer" << BSON("$ref" << "collection" << "$id" << "57e193d7a9cc81b4027498b1")),
JsonCompatible::no},
// code
{BSONCode("function() {}"),
BSON("$code" << "function() {}"),
BSON("$code" << "function() {}"),
JsonCompatible::no},
// symbol
{BSONSymbol("symbol"),
BSON("$symbol" << "symbol"),
BSON("$symbol" << "symbol"),
JsonCompatible::no},
// codeWScope
{BSONCodeWScope("function() {}", BSON("n" << 5)),
BSON("$code" << "function() {}" << "$scope" << BSON("n" << BSON("$numberInt" << "5"))),
// the $scope is always in canonical format
BSON("$code" << "function() {}" << "$scope" << BSON("n" << BSON("$numberInt" << "5"))),
JsonCompatible::no},
// numberInt
{42, BSON("$numberInt" << "42"), 42, JsonCompatible::yes},
// timestamp
{Timestamp(42, 1),
BSON("$timestamp" << BSON("t" << 42 << "i" << 1)),
BSON("$timestamp" << BSON("t" << 42 << "i" << 1)),
JsonCompatible::no},
// numberLong
{42LL, BSON("$numberLong" << "42"), 42LL, JsonCompatible::yes},
// numberDecimal
{Decimal128(42.42),
BSON("$numberDecimal" << "42.4200000000000"),
BSON("$numberDecimal" << "42.4200000000000"),
JsonCompatible::no},
{Decimal128::kPositiveInfinity,
BSON("$numberDecimal" << "Infinity"),
BSON("$numberDecimal" << "Infinity"),
JsonCompatible::no},
{Decimal128::kNegativeInfinity,
BSON("$numberDecimal" << "-Infinity"),
BSON("$numberDecimal" << "-Infinity"),
JsonCompatible::no},
{Decimal128::kPositiveNaN,
BSON("$numberDecimal" << "NaN"),
BSON("$numberDecimal" << "NaN"),
JsonCompatible::no},
{Decimal128::kNegativeNaN,
BSON("$numberDecimal" << "NaN"),
BSON("$numberDecimal" << "NaN"),
JsonCompatible::no},
// maxKey
{MAXKEY, BSON("$maxKey" << 1), BSON("$maxKey" << 1), JsonCompatible::no},
};
/**
* Generate Extended JSON string using the native BSONObj method.
* This is considered the golden truth value.
*/
std::string jsonString(const Value& v, bool relaxed) {
BSONArrayBuilder builder;
v.addToBsonArray(&builder);
BSONArray bsonArr = builder.arr();
auto format = relaxed ? JsonStringFormat::ExtendedRelaxedV2_0_0
: JsonStringFormat::ExtendedCanonicalV2_0_0;
return bsonArr.firstElement().jsonString(format, false, false);
}
/**
* Disable the generator for types where the conversion does not follow the spec.
*/
bool isGeneratorBugged(BSONType t) {
switch (t) {
case BSONType::dbRef:
// ExtendedCanonicalV200Generator does not emit $dbPointer.
return true;
default:
return false;
}
}
/**
* Test against the native BSONObj Extended JSON method.
*/
void testGenerator(const Value& v, bool relaxed) {
auto golden = jsonString(v, relaxed);
auto test = jsonString(serializeToExtendedJson(v, relaxed), true);
ASSERT_EQ(golden, test) << (relaxed ? "relaxed" : "canonical") << " format";
}
TEST(SerializeExtendedJsonUtilsTest, SerializeSucceedsOnTestCases) {
for (auto& tc : testCases) {
auto testCanonical = serializeToExtendedJson(tc.bson, false);
auto testRelaxed = serializeToExtendedJson(tc.bson, true);
if (!tc.bson.missing() && !isGeneratorBugged(tc.bson.getType())) {
testGenerator(tc.bson, false);
testGenerator(tc.bson, true);
}
ASSERT_VALUE_EQ(testCanonical, tc.canonical);
ASSERT_VALUE_EQ(testRelaxed, tc.relaxed);
}
}
TEST(SerializeExtendedJsonUtilsTest, SerializeThrowsOnMissingValues) {
ASSERT_THROWS_CODE(serializeToExtendedJson(Value(), false), DBException, ErrorCodes::BadValue);
ASSERT_THROWS_CODE(serializeToExtendedJson(Value(), true), DBException, ErrorCodes::BadValue);
}
Value makeNested(Value v, int depth) {
std::vector<Value> next{std::move(v)};
for (int i = 0; i < depth; i++) {
next = std::vector<Value>{Value(std::move(next))};
}
return Value(std::move(next));
}
TEST(SerializeExtendedJsonUtilsTest, SerializeThrowsOnNestingLimit) {
int depthLimit = BSONDepth::getMaxAllowableDepth();
auto nested = makeNested(Value(1), depthLimit - 2);
// Does not throw
serializeToExtendedJson(nested, true);
// Throws because of added nesting level {$numberInt: "1"}.
ASSERT_THROWS_CODE(
serializeToExtendedJson(nested, false), DBException, ErrorCodes::ConversionFailure);
}
TEST(SerializeExtendedJsonUtilsTest, SerializeThrowsOnSizeLimit) {
const size_t sizeLimit = BSONObjMaxUserSize;
// Compute string length required for our object.
size_t stringLengthToHitLimit = sizeLimit - BSON("string" << "" << "number" << 1).objsize();
auto largeObject = BSON("string" << std::string(stringLengthToHitLimit, 'x') << "number" << 1);
ASSERT_EQ(largeObject.objsize(), BSONObjMaxUserSize);
auto largeValue = Value(largeObject);
// Does not throw
serializeToExtendedJson(largeValue, true);
// Throws because of added type wrapper {$numberInt: "1"}.
ASSERT_THROWS_CODE(
serializeToExtendedJson(largeValue, false), DBException, ErrorCodes::ConversionFailure);
}
} // namespace
} // namespace mongo::exec::expression::serialize_ejson_utils