std::unique_ptr<QuerySolution> QueryPlannerAnalysis::analyzeDataAccess(
    const CanonicalQuery& query,
    const QueryPlannerParams& params,
    std::unique_ptr<QuerySolutionNode> solnRoot) {
    auto soln = std::make_unique<QuerySolution>();
    soln->filterData = query.getQueryObj();
    soln->indexFilterApplied = params.indexFiltersApplied;

    solnRoot->computeProperties();

    analyzeGeo(params, solnRoot.get());

    // solnRoot finds all our results.  Let's see what transformations we must perform to the
    // data.

    // If we're answering a query on a sharded system, we need to drop documents that aren't
    // logically part of our shard.
    if (params.options & QueryPlannerParams::INCLUDE_SHARD_FILTER) {
        if (!solnRoot->fetched()) {
            // See if we need to fetch information for our shard key.
            // NOTE: Solution nodes only list ordinary, non-transformed index keys for now

            bool fetch = false;
            BSONObjIterator it(params.shardKey);
            while (it.more()) {
                BSONElement nextEl = it.next();
                if (!solnRoot->hasField(nextEl.fieldName())) {
                    fetch = true;
                    break;
                }
            }

            if (fetch) {
                FetchNode* fetchNode = new FetchNode();
                fetchNode->children.push_back(solnRoot.release());
                solnRoot.reset(fetchNode);
            }
        }

        ShardingFilterNode* sfn = new ShardingFilterNode();
        sfn->children.push_back(solnRoot.release());
        solnRoot.reset(sfn);
    }

    bool hasSortStage = false;
    solnRoot.reset(analyzeSort(query, params, solnRoot.release(), &hasSortStage));

    // This can happen if we need to create a blocking sort stage and we're not allowed to.
    if (!solnRoot) {
        return nullptr;
    }

    // A solution can be blocking if it has a blocking sort stage or
    // a hashed AND stage.
    bool hasAndHashStage = hasNode(solnRoot.get(), STAGE_AND_HASH);
    soln->hasBlockingStage = hasSortStage || hasAndHashStage;

    const QueryRequest& qr = query.getQueryRequest();

    if (qr.getSkip()) {
        auto skip = std::make_unique<SkipNode>();
        skip->skip = *qr.getSkip();
        skip->children.push_back(solnRoot.release());
        solnRoot = std::move(skip);
    }

    // Project the results.
    if (query.getProj()) {
        solnRoot = analyzeProjection(query, std::move(solnRoot), hasSortStage);
        // If we don't have a covered project, and we're not allowed to put an uncovered one in,
        // bail out.
        if (solnRoot->fetched() && params.options & QueryPlannerParams::NO_UNCOVERED_PROJECTIONS)
            return nullptr;
    } else {
        // If there's no projection, we must fetch, as the user wants the entire doc.
        if (!solnRoot->fetched() && !(params.options & QueryPlannerParams::IS_COUNT)) {
            FetchNode* fetch = new FetchNode();
            fetch->children.push_back(solnRoot.release());
            solnRoot.reset(fetch);
        }
    }

    // When there is both a blocking sort and a limit, the limit will
    // be enforced by the blocking sort.
    // Otherwise, we need to limit the results in the case of a hard limit
    // (ie. limit in raw query is negative)
    if (!hasSortStage) {
        // We don't have a sort stage. This means that, if there is a limit, we will have
        // to enforce it ourselves since it's not handled inside SORT.
        if (qr.getLimit()) {
            LimitNode* limit = new LimitNode();
            limit->limit = *qr.getLimit();
            limit->children.push_back(solnRoot.release());
            solnRoot.reset(limit);
        } else if (qr.getNToReturn() && !qr.wantMore()) {
            // We have a "legacy limit", i.e. a negative ntoreturn value from an OP_QUERY style
            // find.
            LimitNode* limit = new LimitNode();
            limit->limit = *qr.getNToReturn();
            limit->children.push_back(solnRoot.release());
            solnRoot.reset(limit);
        }
    }

    soln->root = std::move(solnRoot);
    return soln;
}