ExportXMLWordPrintableJSON

    • Type: Bug
    • Resolution: Won't Fix
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Storage Execution
    • ALL
    • Hide
      TEST(MinMax, MixedObjectAndArrayProducesWrongMaxArray) {
          tracking::Context trackingContext;
          MinMax minmax{trackingContext};
      
          for (auto&& measurement : {BSON("p" << BSON_ARRAY(3)),
                                     BSON("p" << BSON("b" << 2)),
                                     BSON("p" << BSON("a" << 1)),
                                     BSON("p" << BSON_ARRAY(4))}) {
              minmax.update(measurement, /*omitField=*/boost::none, nullptr);
          }
      
          // The min is correct for this sequence, which isolates the defect to the max.
          ASSERT_BSONOBJ_EQ(BSON("p" << BSON("a" << 1 << "b" << 2)), minmax.min());
      
           / Objects sort below arrays, so only the two array-typed measurements contribute to the max,
          // and the element-wise max of [3] and [4] is [4]. Fails today, the aliasing yields [4, 3].
          ASSERT_BSONOBJ_EQ(BSON("p" << BSON_ARRAY(4)), minmax.max());
        }
      
      Show
      TEST(MinMax, MixedObjectAndArrayProducesWrongMaxArray) { tracking::Context trackingContext; MinMax minmax{trackingContext}; for ( auto && measurement : {BSON( "p" << BSON_ARRAY(3)), BSON( "p" << BSON( "b" << 2)), BSON( "p" << BSON( "a" << 1)), BSON( "p" << BSON_ARRAY(4))}) { minmax.update(measurement, /*omitField=*/ boost::none, nullptr ); } // The min is correct for this sequence, which isolates the defect to the max. ASSERT_BSONOBJ_EQ(BSON( "p" << BSON( "a" << 1 << "b" << 2)), minmax.min()); / Objects sort below arrays, so only the two array-typed measurements contribute to the max, // and the element-wise max of [3] and [4] is [4]. Fails today, the aliasing yields [4, 3]. ASSERT_BSONOBJ_EQ(BSON( "p" << BSON_ARRAY(4)), minmax.max()); }
    • Storage Execution 2026-09-14, Storage Execution 2026-09-28
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      MinMax computes the wrong control.max for any time-series field path that is an object in some measurements of a bucket and an array in others. The emitted array can have the wrong length, values at the wrong indices, or entries that correspond to no measurement at all.

      Cause

      FlatBSONStore keeps object field names and array indices for a field path in the same positional list of child entries. Array elements use a reserved placeholder name (kArrayFieldName), object fields use their real names, and both share one child run. An array index is not recorded anywhere, it is implied by the entry's position, and object updates are free to change those positions.

      Three sites combine:

      • _update() array branch binds array element i to the i-th child, whatever its name.
      • _updateObj() and Obj::search() rename a placeholder via claimArrayFieldNameForObject() without clearing the max it holds, and _updateObj() inserts a new field at the current cursor (here and here), which insert-before and lands ahead of existing array entries.
      • _append() emits positionally and skips kUnset entries with no else branch, so a value written at store position k comes back out at a smaller emitted index.

      Only control.max is exposed, because objects sort below arrays in canonical order (typeComp), so the array side of an object/array mix is always the max.

      Worked example, field p: [3], {b: 2}, {a: 1}}, [4].
      
      1. [3] creates one placeholder, max 3.
      2. {b: 2} renames the placeholder to b, which keeps array slot 0's max of 3.
      3. {a: 1} inserts a before b, so b's 3 now means index 1.
      4. [4] binds element 0 to position 0, which is {{a}}.
      

      Result [4, 3], correct answer [4]. The trailing 3 appeared only at index 0, and no measurement has a two-element array.

      Which buckets look broken follows a rule. With k object fields inserted ahead of the claiming entry and a later array of length L:

      • L <= k: the walk never reaches the claimer, its stale value is appended after the new ones, emitted length L+1 with a phantom entry.
      • L > k: the claimer is refreshed, length is right, but a stale value larger than the true value at index k still wins the max.

      Both branches appear in AF-19598, 842 buckets with a wrong length and 85,703 with the right length and wrong contents. Buckets that currently pass validate are not necessarily correct, some may be masked by the second branch.

            Assignee:
            Ernesto Rodriguez Reina
            Reporter:
            Ernesto Rodriguez Reina
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: