ExportXMLWordPrintableJSON

    • Query Execution
    • Minor Change
    • ALL
    • Hide

      Deployment: single-node replica set, default configuration (internalQueryFrameworkControl is trySbeRestricted by default). The script switches the parameter with setParameter and leaves it at the default.

      db.c.insertMany([{_id: 1, k: 1, s: "abc"}]);
      const pipeline = [{$match: {k: 1}}, {$group: {_id: null, r: {$push: {$substrBytes: ["$s", 1.5, 2]}}}}];
      
      // The same aggregation with the classic engine forced, then with the default engine selection.
      for (const mode of ["forceClassicEngine", "trySbeRestricted"]) {
          db.adminCommand({setParameter: 1, internalQueryFrameworkControl: mode});
          const e = db.c.explain().aggregate(pipeline);
          const qp = e.queryPlanner || e.stages[0].$cursor.queryPlanner;
          const plan = [];
          for (let s = qp.winningPlan.queryPlan || qp.winningPlan; s; s = s.inputStage) plan.push(s.stage);
          const rest = (e.stages || []).slice(1).map(s => Object.keys(s)[0]);
          print(`// ${mode}: explainVersion ${e.explainVersion}, plan ${plan.join(" <- ")}` + (rest.length ? `, then ${rest.join(", ")}` : ""));
          printjson(db.c.aggregate(pipeline).toArray());
      }
      

      Output:

      // forceClassicEngine: explainVersion 1, plan PROJECTION_SIMPLE <- COLLSCAN, then $group
      [
        {
          _id: null,
          r: [
            'bc'
          ]
        }
      ]
      // trySbeRestricted: explainVersion 2, plan GROUP <- COLLSCAN
      [
        {
          _id: null,
          r: []
        }
      ]
      
      Show
      Deployment: single-node replica set, default configuration ( internalQueryFrameworkControl is trySbeRestricted by default). The script switches the parameter with setParameter and leaves it at the default. db.c.insertMany([{_id: 1, k: 1, s: "abc" }]); const pipeline = [{$match: {k: 1}}, {$group: {_id: null , r: {$push: {$substrBytes: [ "$s" , 1.5, 2]}}}}]; // The same aggregation with the classic engine forced, then with the default engine selection. for ( const mode of [ "forceClassicEngine" , "trySbeRestricted" ]) { db.adminCommand({setParameter: 1, internalQueryFrameworkControl: mode}); const e = db.c.explain().aggregate(pipeline); const qp = e.queryPlanner || e.stages[0].$cursor.queryPlanner; const plan = []; for (let s = qp.winningPlan.queryPlan || qp.winningPlan; s; s = s.inputStage) plan.push(s.stage); const rest = (e.stages || []).slice(1).map(s => Object .keys(s)[0]); print( ` // ${mode}: explainVersion ${e.explainVersion}, plan ${plan.join( " <- " )}` + (rest.length ? `, then ${rest.join( ", " )}` : "")); printjson(db.c.aggregate(pipeline).toArray()); } Output: // forceClassicEngine: explainVersion 1, plan PROJECTION_SIMPLE <- COLLSCAN, then $group [ { _id: null , r: [ 'bc' ] } ] // trySbeRestricted: explainVersion 2, plan GROUP <- COLLSCAN [ { _id: null , r: [] } ]
    • QE 2026-10-12
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      $substrBytes with a non-integral start index (1.5) gives 'bc' in the classic engine (the index is truncated to 1) and missing in SBE. Inside $group the expression runs in SBE under the default engine selection, so $push collects 'bc' with the classic engine and nothing with SBE.

      • classic engine: internalQueryFrameworkControl: forceClassicEngine. Check: the explain line in the output shows explainVersion 1, with $group after $cursor.
      • SBE: default internalQueryFrameworkControl: trySbeRestricted: the whole pipeline, including $group, runs in SBE. Check: the explain line shows explainVersion 2 with GROUP in the SBE plan.

      Expected: The same result with both engines. The documentation asks for a start index and length that are non-negative integers or numbers representable as integers, so 1.5 is outside the contract; an error from both engines, or the same value from both, would be consistent.
      Actual: Classic engine: r holds 'bc'. Default (trySbeRestricted, SBE): r is an empty array (the expression is missing), with no error.

      The classic engine converts the start index and length with coerceToLong(), which truncates. SBE converts them with convert(…, NumberInt64), which fails for a non-integral value and yields Nothing; builtinSubstrBytes then returns Nothing for a non-Int64 argument. $substrCP agrees between the engines on the same input. master has the same code.

      Found with AI assistance.

            Assignee:
            Vadim Piven
            Reporter:
            r33s3n6 N/A (EXT)
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated:
              Resolved: