/**
* Copyright (C) 2016 MongoDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* 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 GNU Affero General 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/platform/basic.h"
#include
#include
#include "mongo/base/init.h"
#include "mongo/base/status.h"
#include "mongo/base/string_data.h"
#include "mongo/bson/bsonmisc.h"
#include "mongo/bson/bsonobj.h"
#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/db/namespace_string.h"
#include "mongo/db/operation_context.h"
#include "mongo/db/query/collation/collator_factory_interface.h"
#include "mongo/db/query/query_test_service_context.h"
#include "mongo/db/service_context_noop.h"
#include "mongo/db/views/durable_view_catalog.h"
#include "mongo/db/views/view.h"
#include "mongo/db/views/view_catalog.h"
#include "mongo/db/views/view_graph.h"
#include "mongo/stdx/functional.h"
#include "mongo/stdx/memory.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/mongoutils/str.h"
namespace mongo {
namespace {
constexpr auto kLargeString =
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000";
const auto kOneKiBMatchStage = BSON("$match" << BSON("data" << kLargeString));
const auto kTinyMatchStage = BSON("$match" << BSONObj());
class DurableViewCatalogDummy final : public DurableViewCatalog {
public:
explicit DurableViewCatalogDummy() : _upsertCount(0), _iterateCount(0) {}
static const std::string name;
using Callback = stdx::function;
virtual Status iterate(OperationContext* opCtx, Callback callback) {
++_iterateCount;
return Status::OK();
}
virtual void upsert(OperationContext* opCtx, const NamespaceString& name, const BSONObj& view) {
++_upsertCount;
}
virtual void remove(OperationContext* opCtx, const NamespaceString& name) {}
virtual const std::string& getName() const {
return name;
};
int getUpsertCount() {
return _upsertCount;
}
int getIterateCount() {
return _iterateCount;
}
private:
int _upsertCount;
int _iterateCount;
};
const std::string DurableViewCatalogDummy::name = "dummy";
class ViewCatalogFixture : public unittest::Test {
public:
ViewCatalogFixture()
: _queryServiceContext(stdx::make_unique()),
opCtx(_queryServiceContext->makeOperationContext()),
viewCatalog(&durableViewCatalog) {}
private:
std::unique_ptr _queryServiceContext;
protected:
DurableViewCatalogDummy durableViewCatalog;
ServiceContext::UniqueOperationContext opCtx;
ViewCatalog viewCatalog;
const BSONArray emptyPipeline;
const BSONObj emptyCollation;
};
TEST_F(ViewCatalogFixture, CreateExistingView) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
ASSERT_OK(viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
TEST_F(ViewCatalogFixture, CreateViewOnDifferentDatabase) {
const NamespaceString viewName("db1.view");
const NamespaceString viewOn("db2.coll");
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
TEST_F(ViewCatalogFixture, CreateViewWithPipelineFailsOnInvalidStageName) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
auto invalidPipeline = BSON_ARRAY(BSON("INVALID_STAGE_NAME" << 1));
ASSERT_THROWS(
viewCatalog.createView(opCtx.get(), viewName, viewOn, invalidPipeline, emptyCollation)
.transitional_ignore(),
AssertionException);
}
TEST_F(ViewCatalogFixture, CreateViewOnInvalidCollectionName) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.$coll");
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
TEST_F(ViewCatalogFixture, ExceedMaxViewDepthInOrder) {
const char* ns = "db.view";
int i = 0;
for (; i < ViewGraph::kMaxViewDepth; i++) {
const NamespaceString viewName(str::stream() << ns << i);
const NamespaceString viewOn(str::stream() << ns << (i + 1));
ASSERT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
const NamespaceString viewName(str::stream() << ns << i);
const NamespaceString viewOn(str::stream() << ns << (i + 1));
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
TEST_F(ViewCatalogFixture, ExceedMaxViewDepthByJoining) {
const char* ns = "db.view";
int i = 0;
int size = ViewGraph::kMaxViewDepth * 2 / 3;
for (; i < size; i++) {
const NamespaceString viewName(str::stream() << ns << i);
const NamespaceString viewOn(str::stream() << ns << (i + 1));
ASSERT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
for (i = 1; i < size + 1; i++) {
const NamespaceString viewName(str::stream() << ns << (size + i));
const NamespaceString viewOn(str::stream() << ns << (size + i + 1));
ASSERT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
const NamespaceString viewName(str::stream() << ns << size);
const NamespaceString viewOn(str::stream() << ns << (size + 1));
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
TEST_F(ViewCatalogFixture, CreateViewCycles) {
{
const NamespaceString viewName("db.view1");
const NamespaceString viewOn("db.view1");
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
{
const NamespaceString view1("db.view1");
const NamespaceString view2("db.view2");
const NamespaceString view3("db.view3");
ASSERT_OK(viewCatalog.createView(opCtx.get(), view1, view2, emptyPipeline, emptyCollation));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view2, view3, emptyPipeline, emptyCollation));
ASSERT_NOT_OK(
viewCatalog.createView(opCtx.get(), view3, view1, emptyPipeline, emptyCollation));
}
}
TEST_F(ViewCatalogFixture, CanSuccessfullyCreateViewWhosePipelineIsExactlyAtMaxSizeInBytes) {
ASSERT_EQ(ViewGraph::kMaxViewPipelineSizeBytes % kOneKiBMatchStage.objsize(), 0);
BSONArrayBuilder builder(ViewGraph::kMaxViewPipelineSizeBytes);
int pipelineSize = 0;
for (; pipelineSize < ViewGraph::kMaxViewPipelineSizeBytes;
pipelineSize += kOneKiBMatchStage.objsize()) {
builder << kOneKiBMatchStage;
}
ASSERT_EQ(pipelineSize, ViewGraph::kMaxViewPipelineSizeBytes);
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
const BSONObj collation;
ASSERT_OK(viewCatalog.createView(opCtx.get(), viewName, viewOn, builder.arr(), collation));
}
TEST_F(ViewCatalogFixture, CannotCreateViewWhosePipelineExceedsMaxSizeInBytes) {
// Fill the builder to exactly the maximum size, then push it just over the limit by adding an
// additional tiny match stage.
BSONArrayBuilder builder(ViewGraph::kMaxViewPipelineSizeBytes);
for (int pipelineSize = 0; pipelineSize < ViewGraph::kMaxViewPipelineSizeBytes;
pipelineSize += kOneKiBMatchStage.objsize()) {
builder << kOneKiBMatchStage;
}
builder << kTinyMatchStage;
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
const BSONObj collation;
ASSERT_NOT_OK(viewCatalog.createView(opCtx.get(), viewName, viewOn, builder.arr(), collation));
}
TEST_F(ViewCatalogFixture, CannotCreateViewIfItsFullyResolvedPipelineWouldExceedMaxSizeInBytes) {
BSONArrayBuilder builder1;
BSONArrayBuilder builder2;
for (int pipelineSize = 0; pipelineSize < ViewGraph::kMaxViewPipelineSizeBytes;
pipelineSize += (kOneKiBMatchStage.objsize() * 2)) {
builder1 << kOneKiBMatchStage;
builder2 << kOneKiBMatchStage;
}
builder2 << kTinyMatchStage;
const NamespaceString view1("db.view1");
const NamespaceString view2("db.view2");
const NamespaceString viewOn("db.coll");
const BSONObj collation1;
const BSONObj collation2;
ASSERT_OK(viewCatalog.createView(opCtx.get(), view1, viewOn, builder1.arr(), collation1));
ASSERT_NOT_OK(viewCatalog.createView(opCtx.get(), view2, view1, builder2.arr(), collation2));
}
TEST_F(ViewCatalogFixture, DropMissingView) {
NamespaceString viewName("db.view");
ASSERT_NOT_OK(viewCatalog.dropView(opCtx.get(), viewName));
}
TEST_F(ViewCatalogFixture, ModifyMissingView) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
ASSERT_NOT_OK(viewCatalog.modifyView(opCtx.get(), viewName, viewOn, emptyPipeline));
}
TEST_F(ViewCatalogFixture, ModifyViewOnDifferentDatabase) {
const NamespaceString viewName("db1.view");
const NamespaceString viewOn("db2.coll");
ASSERT_NOT_OK(viewCatalog.modifyView(opCtx.get(), viewName, viewOn, emptyPipeline));
}
TEST_F(ViewCatalogFixture, ModifyViewOnInvalidCollectionName) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.$coll");
ASSERT_NOT_OK(viewCatalog.modifyView(opCtx.get(), viewName, viewOn, emptyPipeline));
}
TEST_F(ViewCatalogFixture, LookupMissingView) {
ASSERT(!viewCatalog.lookup(opCtx.get(), "db.view"_sd));
}
TEST_F(ViewCatalogFixture, LookupExistingView) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
ASSERT_OK(viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
ASSERT(viewCatalog.lookup(opCtx.get(), "db.view"_sd));
}
TEST_F(ViewCatalogFixture, CreateViewThenDropAndLookup) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
ASSERT_OK(viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
ASSERT_OK(viewCatalog.dropView(opCtx.get(), viewName));
ASSERT(!viewCatalog.lookup(opCtx.get(), "db.view"_sd));
}
TEST_F(ViewCatalogFixture, ModifyTenTimes) {
const char* ns = "db.view";
int i;
for (i = 0; i < 5; i++) {
const NamespaceString viewName(str::stream() << ns << i);
const NamespaceString viewOn(str::stream() << ns << (i + 1));
ASSERT_OK(
viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
}
for (i = 0; i < 5; i++) {
const NamespaceString viewName(str::stream() << ns << i);
const NamespaceString viewOn(str::stream() << ns << (i + 1));
ASSERT_OK(viewCatalog.modifyView(opCtx.get(), viewName, viewOn, emptyPipeline));
}
ASSERT_EQ(10, durableViewCatalog.getUpsertCount());
}
TEST_F(ViewCatalogFixture, Iterate) {
const NamespaceString view1("db.view1");
const NamespaceString view2("db.view2");
const NamespaceString view3("db.view3");
const NamespaceString viewOn("db.coll");
ASSERT_OK(viewCatalog.createView(opCtx.get(), view1, viewOn, emptyPipeline, emptyCollation));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view2, viewOn, emptyPipeline, emptyCollation));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view3, viewOn, emptyPipeline, emptyCollation));
std::set viewNames = {"db.view1", "db.view2", "db.view3"};
viewCatalog.iterate(opCtx.get(), [&viewNames](const ViewDefinition& view) {
std::string name = view.name().toString();
ASSERT(viewNames.end() != viewNames.find(name));
viewNames.erase(name);
});
ASSERT(viewNames.empty());
}
TEST_F(ViewCatalogFixture, ResolveViewCorrectPipeline) {
const NamespaceString view1("db.view1");
const NamespaceString view2("db.view2");
const NamespaceString view3("db.view3");
const NamespaceString viewOn("db.coll");
BSONArrayBuilder pipeline1;
BSONArrayBuilder pipeline3;
BSONArrayBuilder pipeline2;
pipeline1 << BSON("$match" << BSON("foo" << 1));
pipeline2 << BSON("$match" << BSON("foo" << 2));
pipeline3 << BSON("$match" << BSON("foo" << 3));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view1, viewOn, pipeline1.arr(), emptyCollation));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view2, view1, pipeline2.arr(), emptyCollation));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view3, view2, pipeline3.arr(), emptyCollation));
auto resolvedView = viewCatalog.resolveView(opCtx.get(), view3);
ASSERT(resolvedView.isOK());
std::vector expected = {BSON("$match" << BSON("foo" << 1)),
BSON("$match" << BSON("foo" << 2)),
BSON("$match" << BSON("foo" << 3))};
std::vector result = resolvedView.getValue().getPipeline();
ASSERT_EQ(expected.size(), result.size());
for (uint32_t i = 0; i < expected.size(); i++) {
ASSERT(SimpleBSONObjComparator::kInstance.evaluate(expected[i] == result[i]));
}
}
TEST_F(ViewCatalogFixture, ResolveViewCorrectlyExtractsDefaultCollation) {
const NamespaceString view1("db.view1");
const NamespaceString view2("db.view2");
const NamespaceString viewOn("db.coll");
BSONArrayBuilder pipeline1;
BSONArrayBuilder pipeline2;
pipeline1 << BSON("$match" << BSON("foo" << 1));
pipeline2 << BSON("$match" << BSON("foo" << 2));
BSONObj collation = BSON("locale"
<< "mock_reverse_string");
ASSERT_OK(viewCatalog.createView(opCtx.get(), view1, viewOn, pipeline1.arr(), collation));
ASSERT_OK(viewCatalog.createView(opCtx.get(), view2, view1, pipeline2.arr(), collation));
auto resolvedView = viewCatalog.resolveView(opCtx.get(), view2);
ASSERT(resolvedView.isOK());
ASSERT_EQ(resolvedView.getValue().getNamespace(), viewOn);
std::vector expected = {BSON("$match" << BSON("foo" << 1)),
BSON("$match" << BSON("foo" << 2))};
std::vector result = resolvedView.getValue().getPipeline();
ASSERT_EQ(expected.size(), result.size());
for (uint32_t i = 0; i < expected.size(); i++) {
ASSERT(SimpleBSONObjComparator::kInstance.evaluate(expected[i] == result[i]));
}
auto expectedCollation =
CollatorFactoryInterface::get(opCtx->getServiceContext())->makeFromBSON(collation);
ASSERT_OK(expectedCollation.getStatus());
ASSERT_BSONOBJ_EQ(resolvedView.getValue().getDefaultCollation(),
expectedCollation.getValue()->getSpec().toBSON());
}
TEST_F(ViewCatalogFixture, InvalidateThenReload) {
const NamespaceString viewName("db.view");
const NamespaceString viewOn("db.coll");
ASSERT_OK(viewCatalog.createView(opCtx.get(), viewName, viewOn, emptyPipeline, emptyCollation));
ASSERT_EQ(1, durableViewCatalog.getIterateCount());
ASSERT(viewCatalog.lookup(opCtx.get(), "db.view"_sd));
ASSERT_EQ(1, durableViewCatalog.getIterateCount());
viewCatalog.invalidate();
ASSERT_OK(viewCatalog.reloadIfNeeded(opCtx.get()));
ASSERT_EQ(2, durableViewCatalog.getIterateCount());
}
} // namespace
} // namespace mongo