mirror of https://github.com/mongodb/mongo
SERVER-107499 Implement BSON to Extended JSON (in BSON) conversion (#44519)
GitOrigin-RevId: 9f51ee9e3f7f8dc8e7003232248380093ec64b64
This commit is contained in:
parent
839610ff63
commit
2418787761
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,3 +41,6 @@ filters:
|
|||
- "scoped_timer*":
|
||||
approvers:
|
||||
- 10gen/server-programmability
|
||||
- "serialize_ejson*":
|
||||
approvers:
|
||||
- 10gen/query-optimization
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue