Skip to content

fix(mongoc): a negated q on a sub-attribute path matched Entities without it - #56

Merged
kzangeli merged 1 commit into
mainfrom
test/q-deep-path-negated-oracle
Sep 3, 2026
Merged

fix(mongoc): a negated q on a sub-attribute path matched Entities without it#56
kzangeli merged 1 commit into
mainfrom
test/q-deep-path-negated-oracle

Conversation

@kzangeli

@kzangeli kzangeli commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

q=a.b.c!=1 returned Entities that have no a.b.c at all — under mongoc only, which pushes q into the database, while corDB walks the Entity in the broker (ldEntityMatch.c) and answered correctly all along.

The backend contradicted itself

Over a fixture that truncates the path at every depth — E1 a.b.c=1, E2 a.b.c=2, E3 a.b (no c), E4 a (no b), E5 no a:

query mongoc, before corDB § 4.9
q=a.b.c==1 E1 E1 E1
q=a.b.c!=1 E2, E3, E4 E2 E2
q=a.b.c!=99 E1, E2, E3, E4 E1, E2 E1, E2
q=a.b.c E1, E2 E1, E2 E1, E2
q=!a.b.c E3, E4, E5 E3, E4, E5 E3, E4, E5

q=!a.b.c correctly reports E3 and E4 as not having the path, while q=a.b.c!=1 simultaneously reports their a.b.c as unequal to 1. That is a contradiction inside one backend, not a difference of interpretation.

§ 4.9 settles the reading anyway: a term over an attrPath the Entity does not have evaluates to false. An Entity with no a.b.c does not "have a value different from 1".

Cause

In the multi-instance $expr builder — and the comment above it already states the correct rule:

EVERY instance must satisfy != / notPattern, i.e. none satisfies the positive form — but the Attribute must still be present, since an Entity without it does not "have a value different from X".

It enforces that with {"$type":"$<attrPath>"} == "object", which checks the attribute and says nothing about the rest of the path. For a.b.c, E3 holds a, passes the guard, and then $not over a missing field is true. E5 was excluded only because it lacks a — the guard working at the one depth it covers.

Fix

Add the guard the comment intends: the full per-instance expression — sub-path, value and value-path included — must resolve in at least one instance before the negation applies. At depth 0 it costs nothing, since a stored Attribute always has its value, which is why the existing tests never saw this.

The test is an oracle, and it was free

query_q_deep_path_negated.test declares no REQUIRE_DB, so the harness already runs it against both backends — and they evaluate q in completely different places. Any future divergence between the pushdown and the in-broker matcher fails one of the two suite runs on its own, with no special machinery.

Deep attrPaths were tested before this, but only with == and >. The negated operators are exactly where a pushdown and an in-broker walk part company, because Mongo's $ne / $nin / $not also match a missing field.

Found by following up the attrInstanceOf entry in the never-entered bucket — zero under mongoc, covered under corDB — which is what flagged the two implementations as never having been compared.

Verification: 635/635 mongoc, 585/585 corDB. Both backends now produce byte-identical output against one set of expects.

…hout it

q=a.b.c!=1 returned Entities that have no a.b.c at all. Under mongoc
only - it pushes `q` into the database, while corDB walks the Entity in
the broker (ldEntityMatch.c) and answered correctly all along.

The backend contradicted ITSELF, which is what makes this unambiguous
rather than a matter of spec reading. Over a fixture that truncates the
path at every depth:

  E1  a.b.c = 1      E2  a.b.c = 2      E3  a.b (no c)
  E4  a (no b)       E5  no a at all

  q=!a.b.c    ->  E3, E4, E5          "these do not have the path"
  q=a.b.c!=1  ->  E2, E3, E4          "...and E3/E4's a.b.c is not 1"

§ 4.9 is explicit that a term over an attrPath the Entity does not have
evaluates to FALSE: an Entity with no a.b.c does not "have a value
different from 1".

The cause is in the multi-instance $expr builder, and the comment above
it already states the right rule - "the Attribute must still be present,
since an Entity without it does not have a value different from X". It
enforces that with {"$type":"$<attr>"} == "object", which checks the
ATTRIBUTE and says nothing about the rest of the path. For a.b.c, E3
holds `a`, passes the guard, and then $not over a missing field is true.
E5 was excluded only because it lacks `a` - the guard working at the one
depth it covers.

So the second guard is added: the full per-instance expression - sub-path,
value and value-path included - must resolve in at least one instance
before the negation applies. At depth 0 it costs nothing, a stored
Attribute always has its value, which is why the existing tests never saw
this.

The test needs no machinery to be an oracle. It declares no REQUIRE_DB,
so the harness already runs it against both backends, and they evaluate
`q` in completely different places - any future divergence fails one of
the two suite runs on its own. Deep attrPaths were tested before, but
only with == and >; the negated operators are exactly where a pushdown
and an in-broker walk part company, because Mongo's $ne / $nin / $not
also match a missing field.

635/635 mongoc, 585/585 corDB.
@kzangeli
kzangeli merged commit ce3480f into main Sep 3, 2026
7 checks passed
@kzangeli
kzangeli deleted the test/q-deep-path-negated-oracle branch September 3, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant