Operator Support
Operator Support
Section titled “Operator Support”This page is the complete, static reference for the condition operators the XrmGhost mock QueryEngine answers. It covers all 89 members of the condition-operator inventory the pinned Dataverse SDK declares — nothing is filtered out and nothing is summarised away.
It is transcribed by hand from frozen snapshots of xrmghost-framework-host that live in this
repository next to the page. See Provenance for the commit, the SDK pin and the date.
The Three Dispositions
Section titled “The Three Dispositions”“Supported” is not a yes/no here. It is one of three dispositions the registry declares, and the difference between the two negative ones is the point:
| Disposition | Meaning |
|---|---|
yes | An evaluator is registered for that canonical key and the engine can answer the operator. |
no (deliberate) | The engine refuses the operator on purpose, and the Reason says why — a decision with a cause behind it. |
no | No evaluator is registered and nothing has been decided: a gap, not a judgement. |
Both frontends resolve a condition to the same canonical key and consult the same registry, and the
reason behind a refusal travels with it into the NotSupportedQueryOperatorException message.
That is a statement about how this engine dispatches, and not about what the Dataverse platform
accepts on either path. It is also a different question from whether a FetchXML query can name the
operator: that one is asked of FetchXmlOperatorMap and answered in its own column, Reachable
from FetchXML. The two are independent in both directions, and this page carries a case of each —
childof has a FetchXML spelling and is refused, masksselect is fully supported and has no
FetchXML spelling at all.
Nothing on this page is in the third disposition. Every operator is either answered or refused with a stated reason:
| Operators in the SDK inventory | 89 |
| Supported | 88 |
| Not supported | 1 |
| — of those, deliberately rejected with a reason | 1 |
| — of those, not covered and not classified | 0 |
| Supported but not reachable from FetchXML | 1 |
How to Read a Row
Section titled “How to Read a Row”- Canonical key is the name every part of the engine uses for the operator: the SDK member name
lowercased, derived the way
QueryExpressionFrontendderives it. - SDK member and SDK value are the
Microsoft.Xrm.Sdk.Query.ConditionOperatormember and its integer value in the pinned inventory. - FetchXML spelling is what you write in a
<condition operator="...">. It is not a separate operator: spellings such asnot-begin-withorlast-seven-daysare translated to the canonical key before anything is looked up.EqualandNotEqualeach accept more than one spelling, which is why 91 spellings cover 88 of the 89 operators. The 89th ismasksselect, which has no FetchXML spelling — the cell is—because there is nothing to write, not because nobody wrote it down. - Reachable from FetchXML is the separate question of whether a FetchXML query can name the
operator at all.
yesmeans the frontend resolves some spelling onto the canonical key;no (no FetchXml grammar)means it resolves none. It is a statement about the dialect and never about coverage. - Context required is what the evaluator declares it needs beyond the record itself. See Execution Context.
- Reason is filled in only for a deliberate refusal.
A FetchXML Spelling Is Not Support
Section titled “A FetchXML Spelling Is Not Support”Resolving a spelling and supporting an operator are two different questions with two different
answers. The spelling table answers “which operator is this”; whether the engine can evaluate it is
IConditionEvaluatorRegistry.IsSupported’s answer — and the two come apart in both directions,
which is why Supported and Reachable from FetchXML are separate columns:
- A spelling without support.
child-ofis parsed by the FetchXML frontend and the identity is then declined: the operator isno (deliberate)under Supported andyeshere. Never read the presence of a spelling as evidence of support. - Support without a spelling.
masksselectis answered by the registry and reachable from aQueryExpression, and no FetchXML spelling resolves onto it. It isyesunder Supported andno (no FetchXml grammar)here. Never read the absence of a spelling as a gap in coverage.
The Operators
Section titled “The Operators”Scalar comparison — 12 operators
Section titled “Scalar comparison — 12 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
equal | Equal | 0 | eq, equal | yes | yes | none | — |
notequal | NotEqual | 1 | ne, neq, notequal | yes | yes | none | — |
greaterthan | GreaterThan | 2 | gt | yes | yes | none | — |
greaterequal | GreaterEqual | 4 | ge | yes | yes | none | — |
lessthan | LessThan | 3 | lt | yes | yes | none | — |
lessequal | LessEqual | 5 | le | yes | yes | none | — |
in | In | 8 | in | yes | yes | none | — |
notin | NotIn | 9 | not-in | yes | yes | none | — |
between | Between | 10 | between | yes | yes | none | — |
notbetween | NotBetween | 11 | not-between | yes | yes | none | — |
null | Null | 12 | null | yes | yes | none | — |
notnull | NotNull | 13 | not-null | yes | yes | none | — |
String matching — 8 operators
Section titled “String matching — 8 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
like | Like | 6 | like | yes | yes | none | — |
notlike | NotLike | 7 | not-like | yes | yes | none | — |
beginswith | BeginsWith | 54 | begins-with | yes | yes | none | — |
doesnotbeginwith | DoesNotBeginWith | 55 | not-begin-with | yes | yes | none | — |
endswith | EndsWith | 56 | ends-with | yes | yes | none | — |
doesnotendwith | DoesNotEndWith | 57 | not-end-with | yes | yes | none | — |
contains | Contains | 49 | contains | yes | yes | none | — |
doesnotcontain | DoesNotContain | 50 | not-contain | yes | yes | none | — |
Multi-select choice membership — 2 operators
Section titled “Multi-select choice membership — 2 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
containvalues | ContainValues | 87 | contain-values | yes | yes | none | — |
doesnotcontainvalues | DoesNotContainValues | 88 | not-contain-values | yes | yes | none | — |
Bitwise — 3 operators
Section titled “Bitwise — 3 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
mask | Mask | 46 | mask | yes | yes | none | — |
masksselect | MasksSelect | 48 | — | yes | no (no FetchXml grammar) | none | — |
notmask | NotMask | 47 | not-mask | yes | yes | none | — |
masksselect is answered by the registry and reached by a QueryExpression; the dash under
FetchXML spelling is the absence of a keyword and not a missing transcription. See
The Three Bitwise Operators Are Not Privilege Masks
for the predicate, and Why masksselect has no FetchXML spelling
for the measurement behind the dash.
Explicit date — 4 operators
Section titled “Explicit date — 4 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
on | On | 25 | on | yes | yes | UserTimeZone | — |
noton | NotOn | 52 | not-on | yes | yes | UserTimeZone | — |
onorbefore | OnOrBefore | 26 | on-or-before | yes | yes | UserTimeZone | — |
onorafter | OnOrAfter | 27 | on-or-after | yes | yes | UserTimeZone | — |
Named calendar period — 14 operators
Section titled “Named calendar period — 14 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
yesterday | Yesterday | 14 | yesterday | yes | yes | CurrentInstant, UserTimeZone | — |
today | Today | 15 | today | yes | yes | CurrentInstant, UserTimeZone | — |
tomorrow | Tomorrow | 16 | tomorrow | yes | yes | CurrentInstant, UserTimeZone | — |
last7days | Last7Days | 17 | last-seven-days | yes | yes | CurrentInstant, UserTimeZone | — |
next7days | Next7Days | 18 | next-seven-days | yes | yes | CurrentInstant, UserTimeZone | — |
lastweek | LastWeek | 19 | last-week | yes | yes | CurrentInstant, UserTimeZone, WeekStart | — |
thisweek | ThisWeek | 20 | this-week | yes | yes | CurrentInstant, UserTimeZone, WeekStart | — |
nextweek | NextWeek | 21 | next-week | yes | yes | CurrentInstant, UserTimeZone, WeekStart | — |
lastmonth | LastMonth | 22 | last-month | yes | yes | CurrentInstant, UserTimeZone | — |
thismonth | ThisMonth | 23 | this-month | yes | yes | CurrentInstant, UserTimeZone | — |
nextmonth | NextMonth | 24 | next-month | yes | yes | CurrentInstant, UserTimeZone | — |
lastyear | LastYear | 28 | last-year | yes | yes | CurrentInstant, UserTimeZone | — |
thisyear | ThisYear | 29 | this-year | yes | yes | CurrentInstant, UserTimeZone | — |
nextyear | NextYear | 30 | next-year | yes | yes | CurrentInstant, UserTimeZone | — |
Rolling count of units — 10 operators
Section titled “Rolling count of units — 10 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
lastxhours | LastXHours | 31 | last-x-hours | yes | yes | CurrentInstant, UserTimeZone | — |
nextxhours | NextXHours | 32 | next-x-hours | yes | yes | CurrentInstant, UserTimeZone | — |
lastxdays | LastXDays | 33 | last-x-days | yes | yes | CurrentInstant, UserTimeZone | — |
nextxdays | NextXDays | 34 | next-x-days | yes | yes | CurrentInstant, UserTimeZone | — |
lastxweeks | LastXWeeks | 35 | last-x-weeks | yes | yes | CurrentInstant, UserTimeZone | — |
nextxweeks | NextXWeeks | 36 | next-x-weeks | yes | yes | CurrentInstant, UserTimeZone | — |
lastxmonths | LastXMonths | 37 | last-x-months | yes | yes | CurrentInstant, UserTimeZone | — |
nextxmonths | NextXMonths | 38 | next-x-months | yes | yes | CurrentInstant, UserTimeZone | — |
lastxyears | LastXYears | 39 | last-x-years | yes | yes | CurrentInstant, UserTimeZone | — |
nextxyears | NextXYears | 40 | next-x-years | yes | yes | CurrentInstant, UserTimeZone | — |
Older than — 6 operators
Section titled “Older than — 6 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
olderthanxminutes | OlderThanXMinutes | 86 | olderthan-x-minutes | yes | yes | CurrentInstant, UserTimeZone | — |
olderthanxhours | OlderThanXHours | 85 | olderthan-x-hours | yes | yes | CurrentInstant, UserTimeZone | — |
olderthanxdays | OlderThanXDays | 84 | olderthan-x-days | yes | yes | CurrentInstant, UserTimeZone | — |
olderthanxweeks | OlderThanXWeeks | 83 | olderthan-x-weeks | yes | yes | CurrentInstant, UserTimeZone | — |
olderthanxmonths | OlderThanXMonths | 53 | olderthan-x-months | yes | yes | CurrentInstant, UserTimeZone | — |
olderthanxyears | OlderThanXYears | 82 | olderthan-x-years | yes | yes | CurrentInstant, UserTimeZone | — |
Fiscal calendar — 15 operators
Section titled “Fiscal calendar — 15 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
thisfiscalyear | ThisFiscalYear | 58 | this-fiscal-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
thisfiscalperiod | ThisFiscalPeriod | 59 | this-fiscal-period | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
nextfiscalyear | NextFiscalYear | 60 | next-fiscal-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
nextfiscalperiod | NextFiscalPeriod | 61 | next-fiscal-period | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
lastfiscalyear | LastFiscalYear | 62 | last-fiscal-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
lastfiscalperiod | LastFiscalPeriod | 63 | last-fiscal-period | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
lastxfiscalyears | LastXFiscalYears | 64 | last-x-fiscal-years | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
lastxfiscalperiods | LastXFiscalPeriods | 65 | last-x-fiscal-periods | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
nextxfiscalyears | NextXFiscalYears | 66 | next-x-fiscal-years | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
nextxfiscalperiods | NextXFiscalPeriods | 67 | next-x-fiscal-periods | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
infiscalyear | InFiscalYear | 68 | in-fiscal-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
infiscalperiod | InFiscalPeriod | 69 | in-fiscal-period | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
infiscalperiodandyear | InFiscalPeriodAndYear | 70 | in-fiscal-period-and-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
inorbeforefiscalperiodandyear | InOrBeforeFiscalPeriodAndYear | 71 | in-or-before-fiscal-period-and-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
inorafterfiscalperiodandyear | InOrAfterFiscalPeriodAndYear | 72 | in-or-after-fiscal-period-and-year | yes | yes | CurrentInstant, UserTimeZone, FiscalCalendar | — |
Contextual identity — 9 operators
Section titled “Contextual identity — 9 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
equaluserid | EqualUserId | 41 | eq-userid | yes | yes | UserMembership | — |
notequaluserid | NotEqualUserId | 42 | ne-userid | yes | yes | UserMembership | — |
equalbusinessid | EqualBusinessId | 43 | eq-businessid | yes | yes | UserMembership | — |
notequalbusinessid | NotEqualBusinessId | 44 | ne-businessid | yes | yes | UserMembership | — |
equaluserlanguage | EqualUserLanguage | 51 | eq-userlanguage | yes | yes | UserLanguage | — |
equaluserteams | EqualUserTeams | 73 | eq-userteams | yes | yes | UserTeams | — |
equaluseroruserteams | EqualUserOrUserTeams | 74 | eq-useroruserteams | yes | yes | UserMembership, UserTeams | — |
equaluseroruserhierarchy | EqualUserOrUserHierarchy | 80 | eq-useroruserhierarchy | yes | yes | UserHierarchy | — |
equaluseroruserhierarchyandteams | EqualUserOrUserHierarchyAndTeams | 81 | eq-useroruserhierarchyandteams | yes | yes | UserHierarchy | — |
Structural hierarchy — 5 operators
Section titled “Structural hierarchy — 5 operators”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
above | Above | 78 | above | yes | yes | none | — |
aboveorequal | AboveOrEqual | 79 | eq-or-above | yes | yes | none | — |
under | Under | 75 | under | yes | yes | none | — |
underorequal | UnderOrEqual | 77 | eq-or-under | yes | yes | none | — |
notunder | NotUnder | 76 | not-under | yes | yes | none | — |
Deliberately rejected — 1 operator
Section titled “Deliberately rejected — 1 operator”| Canonical key | SDK member | SDK value | FetchXML spelling | Supported | Reachable from FetchXML | Context required | Reason |
|---|---|---|---|---|---|---|---|
childof | ChildOf | 45 | child-of | no (deliberate) | yes | — | see note |
The One Deliberate Refusal
Section titled “The One Deliberate Refusal”childof is the only operator this engine refuses, and it is not unfinished: it is out for a fact
somebody measured. An evaluator written anyway would answer a query Dataverse itself declines.
Why childof is refused
Section titled “Why childof is refused”The SDK reference marks ChildOf “for internal use only”, and the platform enforces that as a
refusal of use rather than of the name — which is why the operator keeps its FetchXML spelling
and is still no (deliberate):
- Through FetchXML, the frontend parses
child-ofand the platform then rejects the attribute: “The parentbusinessunitid attribute is not a valid attribute for the child-of operator” — ADO-2307 recorded eight rejections out of eight. - Through
QueryExpression, at the operator’s own arity of zero values, the platform answers “The child-of operator is not valid for standard queries. It is for internal crm infrastructure use only via OutlookSync queries.” — ADO-2498, six targets across three hierarchies (businessunit.parentbusinessunitid,businessunit.businessunitid,systemuser.businessunitid,systemuser.parentsystemuserid,role.parentrootroleid,team.businessunitid).
So childof is a reserved operator and not an unknown one, and there is no observed behaviour an
evaluator could mirror. What that measurement did not establish is what ChildOf does inside
OutlookSync, which is a path RetrieveMultiple does not reach.
Why masksselect Has No FetchXML Spelling
Section titled “Why masksselect Has No FetchXML Spelling”masksselect is supported and it is unreachable from FetchXML, and the two halves have to be
read together because either alone reads as the opposite state.
- Every FetchXML spelling tried —
masks-select,masksselect,maskselectandmasks_select— is answered by the Dataverse server with HTTP 400Unknown Condition Operator. That measurement was true, and this engine’s FetchXML frontend mirrors it: none of the spellings that normalise onto the canonical key (masks-select,masksselect,masks_select,MasksSelect,Masks-Select) resolves, so a FetchXML query cannot name the operator under any of them. - A
QueryExpressionreaches it. TheQueryExpressionprobe of ADO-2500, run on 18 September 2026, sentConditionOperator.MasksSelectthroughRetrieveMultipleon four tables (webresource.webresourcetype,roleprivileges.privilegedepthmask,principalobjectaccess.accessrightsmask,systemuser.accessmode) and the server answered 200 with rows on all four, as(value & operand) != 0.
The two measurements do not contradict each other. Together they say the operator exists
server-side and has no FetchXML grammar, which is a fact about a frontend and not a gap in
coverage — so masksselect counts as covered, and its refusal on the FetchXML path is a refusal of
the spelling, not of the operator.
That the server knows the operator rather than ignoring it is visible in its own error messages,
which talk about the operand and never about the operator: the wrong number of values answers
“The ConditonOperator.MasksSelect requires 1 value/s, not 2” — and “not 0” for none — with
Microsoft’s typo included, and a string column answers Sql Number: 402, SQL Server’s operand type
clash, raised by a bitwise AND that reached the SQL generator. An operand of 0 is accepted and
matches nothing, which is what (value & 0) != 0 says.
The Three Bitwise Operators Are Not Privilege Masks
Section titled “The Three Bitwise Operators Are Not Privilege Masks”mask, masksselect and notmask are generic bitwise AND predicates over any integer
attribute, and they are worth stating on their own because the obvious reading of the name is the
wrong one:
| Operator | Predicate | In words |
|---|---|---|
mask | (value & operand) == operand | every bit of the operand is set |
masksselect | (value & operand) != 0 | at least one bit of the operand is set |
notmask | (value & operand) == 0 | no bit of the operand is set |
They are three different predicates, and a single-bit operand hides it. At operand 1 the
readings == operand, != 0 and == bit 0 all coincide, so a counterexample built on a one-bit
operand stays true under any of the three and proves nothing about which one the platform
implements. The discriminating operand is a multi-bit one.
The measured counterexample, on webresource.webresourcetype over the domain
1, 2, 3, 4, 5, 6, 7, 11, 12 at operand 3 (0011):
| Operator | Values the platform matches at operand 3 |
|---|---|
mask | 3, 7, 11 — the only values with both bit 0 and bit 1 set |
masksselect | 1, 2, 3, 5, 6, 7, 11 — every value with either bit set |
notmask | 4, 12 — the values with neither bit set |
Two things follow from that one row, and both correct a reading that a single-bit probe had made plausible:
maskis not!= 0.1,2,5and6each share a bit with3and are matched bymasksselect, and none of them is matched bymask.(value & operand) != 0ismasksselect’s predicate, nevermask’s.maskandnotmaskare not complements.1, 2, 5, 6are in neither. The complement ofnotmaskismasksselect:masksselectis “at least one bit”,notmaskis “no bit”, and together they exhaust the domain.
And the privilege mask is the most famous use of these predicates, not their semantics. The
counterproof is systemuser.accessmode, which is not a privilege column: masksselect 1 returns
two rows, with accessmode values 3 and 5 — each has bit 0 set — and nothing in
answering it reads a privilege, a role or an entitlement. Note that this example is at operand 1,
so it says what the operator does not read; it does not discriminate between the three
predicates, and the operand-3 table above is what does.
All three declare no execution-context requirement: none asks for anything the row cannot supply.
What the bitwise measurement covers, and what it does not
Section titled “What the bitwise measurement covers, and what it does not”The three predicates were confirmed on three columns (webresource.webresourcetype,
principalobjectaccess.accessrightsmask, savedquery.querytype) across ten column/operand
combinations — thirty comparisons between the observed value set and the set the formula predicts,
with zero discrepancies. The bound of that confirmation is stated rather than left to be assumed:
- Types. Only
IntegerandPicklistcolumns were queried.BigInt,DecimalandMoneywere not. A string column answersSql Number: 402, which says there is no implicit conversion but not where the boundary between numeric types lies. - System tables only. No custom attribute was queried, so a SQL generator that treated one differently would not have been seen.
- Range and sign. Every value in every domain fits in a positive
Int32, and the largest operand queried is852023— bit 19. A negative operand, one beyond2^31, and the difference between a 32-bit and a 64-bit AND are all outside what was observed. - One organisation, one instant. The probe ran against a single Dataverse organisation on 18 September 2026, and the server’s own version was not recorded. If the platform changed this behaviour, this measurement would not predict it.
What is outside those bounds is not observed, and this page makes no claim about it either way.
Execution Context
Section titled “Execution Context”An operator that cannot be answered from the record alone declares what it needs to know. The host resolves exactly the union of what the plan’s operators asked for — a plan whose operators need nothing causes no lookup at all.
The Eight Context Requirements
Section titled “The Eight Context Requirements”These are the eight flags of OperatorContextRequirement. They are requirements, not entitlements —
see Not a Licence Surface.
| Requirement | What the operator needs | Where it is read from |
|---|---|---|
CurrentInstant | ”Now” — and every condition of the query agreeing on it | The injected clock, not a record |
UserTimeZone | The caller’s time zone | usersettings (timezonecode, timezonebias) |
WeekStart | The organisation’s first day of the week | organization (weekstartdaycode) |
UserMembership | Who is asking: the caller and the business unit they belong to | usersettings (systemuserid, businessunitid) and systemuser (businessunitid) |
FiscalCalendar | Where the organisation’s fiscal year starts and how long a fiscal period is | organization (fiscalcalendarstart, fiscalperiodtype), plus usersettings for the caller’s offset |
UserLanguage | The caller’s interface language, as an LCID | usersettings (uilanguageid) |
UserTeams | The teams the caller belongs to | teammembership, on systemuserid and teamid and nothing else |
UserHierarchy | The caller and every user below them, plus the teams that whole set belongs to | systemuser (self-referential lookup) and teammembership |
Three separations in that table are load-bearing and are not tidiness:
FiscalCalendaris its own flag, not a widening ofWeekStart, even though both land on theorganizationrecord.thisweekwants the first day of the week and nothing else;thisfiscalyearwants the fiscal anchor and nothing else. That they share a record is the resolver’s business, and it reads that record once for both.FiscalCalendarcarries the caller’s offset with it.organization.fiscalcalendarstartis a date in the user’s local time, so resolving it without an offset would silently read it as UTC. An operator that needs the fiscal calendar does not have to also declareUserTimeZoneto be answered in the right zone.UserHierarchy(bit 128) is not a widening ofUserTeams(bit 64). 64 is already “the teams of the caller”; the hybrid family needs the teams of the caller and of every descendant. Folding them together would makeequaluserteamsenumerate a hierarchy it never reads, or make the hierarchy answer with the caller’s teams alone. Both keep working and both answer wrongly.
UserLanguage is uilanguageid and not localeid. The two sit side by side on the same record
and both read as “the user’s language”: localeid is the formatting locale, uilanguageid is the
language the interface — and the operator — speaks. The resolver deliberately does not read
localeid. Reading the wrong one answers with a plausible number Dataverse would never have matched
on, which is why the attribute is named here rather than inferred.
UserHierarchy is descendants, never ancestors. The two operators behind it answer “mine and my
reports’”, so the traversal runs down the manager chain from the caller. Walking it upwards would
return the caller’s managers, which is a different and plausible-looking answer nothing would flag;
and the business-unit hierarchy is a third thing again, belonging to UserMembership’s table and
not to this one.
The Four Dataverse Records
Section titled “The Four Dataverse Records”The eight requirements above are resolved from four records, which a scenario declares the way it declares any other record. The requirements are the whole input: there is no second list of “context operators” to keep in step with the registry and no typed scenario contract to fill in.
| Record | What it answers | Attributes read |
|---|---|---|
usersettings | The caller’s time zone, business unit and language | systemuserid, businessunitid, uilanguageid, timezonecode, timezonebias |
systemuser | The caller’s business unit when the execution context did not state one, and the user hierarchy below them | systemuserid (primary key), businessunitid; the self-referential lookup Dataverse calls parentsystemuserid |
teammembership | The teams of the caller and of that hierarchy — the platform intersect, one row per (user, team) pair | systemuserid, teamid |
organization | The organisation’s first day of the week and fiscal calendar | organizationid, weekstartdaycode, fiscalcalendarstart, fiscalperiodtype |
Two notes on how records are picked, because both are easy to get wrong:
- The caller is stated, not guessed. The caller is the caller of the organization service the
query goes through:
ExecutionContext.UserId— the user the plugin runs as, notInitiatingUserId— for the service the context provides; the requested user for a service fromCreateOrganizationService(userId); and a fixed internal SYSTEM identity forCreateOrganizationService(null), which has no business unit, no teams and nousersettingsrecord, so its queries run as UTC with no membership. Every per-user record is picked by matching that caller. Reading “the firstusersettingsrecord” instead is correct while a scenario declares one user and quietly answers with a different person’s settings as soon as it declares two. - The
systemuserhierarchy lookup is deduced, not wired. The attribute followed is the single one the registered records carry whose value is anEntityReferencenamingsystemuser.parentsystemuseridis what the platform calls it and is named here because the diagnostics name it — but matching on that name is exactly what the resolver does not do: a scenario declares records, not relationship metadata, and a bareGuidunder that name is an identifier and not a link.
A Missing Record Is a Documented Default, Traced
Section titled “A Missing Record Is a Documented Default, Traced”Nothing here refuses a query for lack of context. A missing record produces a documented default, and each fallback names which record was missing — “some context was defaulted” is not a fix anybody can act on.
Every one of these lines is traced through IEnhancedTracingService under
TraceSource.OperationTracker, so it reaches you as a TraceEntry of the execution response rather
than sitting in a field somebody has to know to look at. Every line carries the prefix
XrmGhost.FrameworkHost.QueryEngine: , so the lines are greppable.
| What was missing | The documented default |
|---|---|
No usersettings record at all | The query runs as UTC with no membership |
No usersettings record names the stated caller | The query runs as UTC with no language |
No caller stated and more than one usersettings record | None is chosen; the query runs as UTC with no language |
The usersettings record carries no timezonebias | The query runs as UTC |
The usersettings record carries no usable uilanguageid | The query runs with no user language |
The usersettings record names neither systemuserid nor businessunitid | The query runs with no membership |
No systemuser record for the caller | The caller’s businessunitid is read from usersettings instead |
No source stated a business unit — not the execution context, not systemuser, not usersettings | The query runs with none — and each of those records that was read and stayed silent is named, with the caller it was read for and what was wrong with it |
No teammembership record at all | The caller belongs to no team |
No teammembership row names the caller with a readable teamid | The caller belongs to no team |
No caller could be established, for UserTeams | The teammembership rows cannot be scoped to anyone; the query runs with no teams |
No organization record, for WeekStart | The week starts on Monday |
No organization record, for FiscalCalendar | The fiscal year starts on 1 January and the fiscal period is monthly |
The organization record carries no usable weekstartdaycode | The week starts on Monday |
The organization record carries no usable fiscalcalendarstart or fiscalperiodtype | 1 January, and monthly — each field falls back on its own, so an unreadable start date does not drag a good period length down with it |
No systemuser record carries an EntityReference to systemuser | No hierarchy could be deduced; the query runs with the caller alone |
No caller could be established, for UserHierarchy | The systemuser hierarchy has nobody to descend from; the query runs with an empty user hierarchy |
One case is a deliberate half-measure and is worth reading twice. When a caller is stated and no
usersettings record names them, but the registry holds exactly one such record, its time
zone is read as the caller’s — and its businessunitid and its uilanguageid are not. The
criterion is what being wrong does: a foreign zone shifts the rows an operator returns, whereas a
foreign business unit or language selects different ones, and a wrong row set never announces
itself. Two or more records and nothing is taken at all. The mismatch is warned about, never hidden.
One missing organization record produces two independent warnings, one per requirement: a plan
that asked only for the fiscal calendar is not told about a week start it never reads, and one that
asked for both is told about both.
The One Thing That Is Not a Default
Section titled “The One Thing That Is Not a Default”A missing record has a documented answer and a defaulted one is still an answer. An unanswerable
hierarchy has none, and inventing one would return rows about a different tree. Three cases
therefore refuse, with a MockHierarchyResolutionException:
- The
systemuserrecords carry two or more self-referential lookups, so which one is the user hierarchy cannot be deduced from them. The refusal names every candidate it found, and the way out is stating the hierarchy in explicit relationship metadata. There is no tie-break here that is not a guess. - The manager chain loops. The refusal names the record that closes the cycle.
- The descent goes deeper than the configured cap. The refusal names the last record reached —
never a truncated set. The default cap is
MockHierarchyResolver.DefaultDepthCap, and the traversal that applies it owns it.
Three things bound that exception, and all three matter:
- It happens only for a plan that declared
UserHierarchy. No other requirement can raise it. - It happens in step 0-bis of
InMemoryExecutor, which is before the first row of the query is read. A refusal raised per row would depend on what the store held, and an empty store would answer an unanswerable query with an empty result set. - It is an
InvalidOperationExceptionunderneath — what the QueryEngine already throws for a structurally unanswerable query — and it is the only way the context resolver ever throws.
Note that zero self-referential lookups is not one of the three. A flat scenario is not a broken one: the answer degrades to the caller alone, traced.
The Five Structural Hierarchy Operators
Section titled “The Five Structural Hierarchy Operators”above, aboveorequal, under, underorequal and notunder say none in the Context
required column, and that is the right answer: where a record sits in a tree has nothing to do with
who is asking, and an operator of this family that declared a context requirement would make every
hierarchy query read usersettings to no purpose.
What it must not be read as is “answerable from the row alone.” What these five need is a
traversal of the records the query itself names, resolved once per plan by InMemoryExecutor before
the first row. They declare it through IHierarchyAwareConditionEvaluator — a separate interface
rather than a member of OperatorContextRequirement, because that enum is the input of the context
resolver, which never sees a condition. “What does the caller’s context hold” is a different
question, and it is the only one the Context required column asks.
They also validate the shape of the condition before the first row: the pivot is a value the condition carries, so “exactly one value, readable as an identifier” is a refusal no store can rescue.
The two hybrid identity operators — equaluseroruserhierarchy and
equaluseroruserhierarchyandteams — are not in this family. They answer about the caller’s
hierarchy and read it off the execution context under UserHierarchy, and they declare no
traversal capability.
Not a Licence Surface
Section titled “Not a Licence Surface”A context requirement says what an operator needs to know, never what the caller is entitled to. Nothing in the requirements, the records or the resolver consults a tier, a licence flag or an entitlement, and nothing downstream turns a requirement into a refusal: an operator that needs context and finds none is evaluated against documented defaults, not gated. Keeping the two apart is deliberate — the moment “this operator needs the caller’s identity” can be read as “this operator is Pro”, a mock query becomes a paywall.
Provenance
Section titled “Provenance”| Repo | xrmghost-framework-host |
| Branch | dev |
| Commit | ff32df7 |
| Snapshot taken | 18 September 2026 |
| SDK pin | Microsoft.CrmSdk.CoreAssemblies 9.0.2.45 |
AssemblyVersion of that assembly | 9.0.0.0 — not the pin |
FileVersion of that assembly | 9.2.47.9489 |
| Inventory transcribed from | docs/query-engine/operator-support-matrix.generated.md |
Why two version numbers appear
Section titled “Why two version numbers appear”AssemblyVersion 9.0.0.0 is the version the shipped Microsoft.Xrm.Sdk assembly carries whatever
package version brought it in: it is 9.0.0.0 for every 9.x package, so it does not identify
the operator inventory this engine is written against. Citing it as the pin would make this
reference non-reproducible, because two different inventories would present themselves under the
same number. The number that does identify the inventory is the package pin,
Microsoft.CrmSdk.CoreAssemblies 9.0.2.45 — and the only place it is visible is
docs/query-engine/sources/OperatorContractTests.cs.txt. The generated matrix does not carry it and
says so: its header states that the shipped assembly carries version 9.0.0.0 whatever package
version brought it in, and refers to that test for the pin. So the pin cannot be taken from the
matrix, and a reader who takes a version number from there is reading the AssemblyVersion.
Why the count is 89
Section titled “Why the count is 89”89 is the size of the condition-operator inventory of the pinned package. The SDK’s inventory
grew to 90 members from 9.0.2.49 (May 2023), which added EqualRoleBusinessId. This engine
stays pinned to 9.0.2.45, so 89 is the right number for this page — and 88 of 89 is the
right coverage claim. A page that quoted 90 would be describing an SDK this engine is not written
against.
How this page is kept true
Section titled “How this page is kept true”This page is transcribed by hand; there is no generator behind it, no CI comparison between the
two repositories and no automatic cross-repo link. When the engine changes, the snapshots are
replaced whole from a new commit and this page is re-transcribed and re-reviewed against them; that
comparison against the sources is the review, and no tool performs it. The only automated gate
on this flow is npm run build, which proves the site compiles — not that the page tells the truth.
The authoritative procedure — the order the snapshots must be replaced in, and the failure mode to
avoid — is the contributor guide,
How to Add a New Docs Page: that is the published one to
follow. The snapshots themselves are documented next to them, in
docs/query-engine/sources/README.md, which records what each copy answers for and the provenance
it was taken at.