Require LiteParserOptions in all nested-pipeline lite-parsed stage constructors

XMLWordPrintableJSON

    • Type: Task
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • Query Integration
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      h3. Problem
      
      LiteParsedDocumentSourceNestedPipelines currently offers two constructor
      families: one taking const LiteParserOptions& and one not. The options-taking form is the only one that populates the base class's {{_ifrContext}} member, read via {{getIfrContext()}}.
      
      Every IFR kickback guard is written in the form:
      
      {code:cpp}
      if (const auto& ifrContext = getIfrContext(); ifrContext &&
          !ifrContext->getSavedFlagValue(flag)) { ...raise kickback... }
      

      The problem is that a stage can end up with a null context, and we may make the wrong decision based on this missing context wrt ifr flag features. 

      It was the root cause of the $lookup failure investigated under SERVER-132516: LiteParsedLookUp::getIfrContext() was always
      null because LiteParsedLookUp's constructor used the no-options single-pipeline
      overload. Fixed there by adding an options-taking variant and threading options
      through, but only for $lookup.

      Why the other stages were missed

      Not a deliberate choice. The no-options constructors predate LiteParserOptions
      entirely (5177f04b2af8). The options-taking overloads were added in exactly two
      commits, each covering only the stage its author was working on at the time:

      • 7dd2520d1e82 (SERVER-125517, view infrastructure) – added the base
        LiteParsedDocumentSource(originalBson, options) ctor and the
        vector<OwnedLiteParsedPipeline> variant, and switched $graphLookup onto it.
      • faec23deb8f4 (SERVER-132516) – added the single-pipeline variant for $lookup.

      Everything else simply kept the older constructor. Notably, 7dd2520d1e82 edited
      lite_parsed_union_with.cpp (in getStageParams()) without the constructor ever
      coming into scope.

      Scope

      Stages still using a no-options constructor, i.e. getIfrContext() is null by
      construction:

      Stage Location
      $unionWith src/mongo/db/pipeline/lite_parsed_union_with.cpp:35
      $merge src/mongo/db/pipeline/document_source_merge.h:87
      $rankFusion src/mongo/db/pipeline/lite_parsed_rank_fusion.h:87
      $scoreFusion src/mongo/db/pipeline/lite_parsed_score_fusion.h:87
      $facet src/mongo/db/pipeline/document_source_facet.h:112
      $out src/mongo/db/pipeline/document_source_out.h:70
      InternalDocumentResultsAndMetadata src/mongo/db/pipeline/lite_parsed_internal_document_results_and_metadata.h:89
      LiftSubpipeline (test stage) src/mongo/db/pipeline/lite_parsed_desugarer_test.cpp:63

      All eight parse() functions already receive a const LiteParserOptions&, so
      threading it through is mechanical.

      Proposed work

      As part of this ticket, investigate feasibility of removing the no-options constructors from: 

        lite_parsed_document_source_nested_pipelines.h outright, rather than
        converting call sites and leaving the overloads in place. Since the only
        behavioural difference is whether _ifrContext is populated – and no stage
        relies on a deliberate null – keeping them preserves a silent-wrong-answer
        footgun for no benefit. Deleting them also resolves the boost::none foreignNss
        overload-resolution ambiguity, because const LiteParserOptions& then always
        occupies parameter position 2.
      Thread options through the eight constructors and their parse() call sites.

      buildUnionWithLPDS() (lite_parsed_hybrid_search_desugarer_utils.cpp:225) is the

        one non-mechanical case: it synthesizes a $unionWith during hybrid-search
        LP-desugaring and has no options in scope. Once $rankFusion/$scoreFusion carry a
        real context, the desugarer can construct
        {{LiteParserOptions

      {.ifrContext = stage.getIfrContext()}

      }} and thread it through
        appendInputPipeline().
      Add a regression test asserting getIfrContext() is non-null for every

        nested-pipeline stage parsed with options carrying an IFR context, recursing
        into sub-pipelines. This is the guard that would have caught the original
        $lookup bug.

      Risk

      Behaviour-neutral. The stored _ifrContext has exactly one reader in the tree
      today, LiteParsedLookUp::validate() (lite_parsed_lookup.cpp:267), added by
      SERVER-132516. DocumentSourceExtensionOptimizable::LiteParsedExpanded declares its
      own _ifrContext member (document_source_extension_optimizable.h:406) populated via
      an explicit constructor argument, so it is unaffected. $graphLookup passes options
      but never reads the result.

      The desugarer change in item 3 is also neutral: the sub-pipeline stages inside a
      synthesized $unionWith are already parsed with the real options by
      LiteParsedRankFusion::parse. Only the synthetic $unionWith wrapper itself gains a
      context, and nothing reads it.

      Out of scope

      Leaf stages remain exposed to the same failure mode. LiteParsedDocumentSource(originalBson)
      and the DEFINE_LITE_PARSED_STAGE_DEFAULT_* macros construct without options and
      have hundreds of call sites. Anyone adding an IFR kickback guard to a stage without
      sub-pipelines will hit this bug again. Closing that gap should be its own ticket.

            Assignee:
            Unassigned
            Reporter:
            Santiago Roche
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated: