fix(iris_interop_query): pin search_table to the extent's document class - #412
Open
PYDuquesnoy wants to merge 1 commit into
Open
PYDuquesnoy wants to merge 1 commit into
PYDuquesnoy wants to merge 1 commit into
Conversation
A Search Table's DocId IS Ens.MessageHeader.MessageBodyId, but MessageBodyId is unique only within one document class's extent. Joining on it alone also matched a custom %Persistent body whose numeric ID collided with an indexed document's ID, so Pkg.MSG.* headers came back beside the HL7 ones. This was a sibling asymmetry. Both joins were added by #4; build_body_join_sql pinned h.MessageBodyClassName and carried a comment giving the reason, and build_search_table_sql did not. The class filter that did exist on this path comes from header_filters and only fires when the caller passes message_class, which is why it read as a caller obligation rather than a defect. The document class is resolved from the extent's DOCCLASS class parameter, and %Dictionary.CompiledClass.PrimarySuper is a ~-delimited list that includes the class itself, so one LIKE returns DOCCLASS and every subclass of it in a single round trip with no HL7 special case. Measured on IRIS for Health 2026.1: EnsLib.HL7.SearchTable -> 1 class, EnsLib.XML.SearchTable -> 5. A bare equality, which is what the report suggested, would have dropped four real body classes for that second extent, trading over-matching for under-matching — the worse of the two, because the caller cannot see it. An extent whose document class cannot be resolved leaves the join unpinned and says so in a warning naming message_class as the remedy. Three mechanisms reach that state (no DOCCLASS, DOCCLASS naming an uncompiled class, an empty DOCCLASS) and all three were measured to return zero rows rather than a pin that silently matches everything. Emitting IN () or 1=0 instead would answer with zero rows, and zero rows from a search read as "no message matched" — the negative-fact shape this repo keeps removing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N7fLbq3ftb82ub45QCpsPB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #409.
iris_interop_query(what=search_table)joinedSearchTable.DocId = Ens.MessageHeader.MessageBodyIdand stopped there.
MessageBodyIdis unique only within one document class's extent, so a custom%Persistentbody whose numeric ID collided with an indexed document's ID came back too — thereporter saw
Pkg.MSG.*headers returned next to the HL7 ones.This was a sibling asymmetry, not a missing idea
Both joins in
interop.rswere added by #4. One pinned the class; the other did not.h.MessageBodyClassName?build_body_join_sql(body-class join)build_search_table_sql(search-table join)And the one that did carries a doc comment giving the reporter's own reason:
So the hazard was understood when the code was written and only one of the two siblings got the
guard.
neither_join_leaves_the_body_class_unconstrainedtherefore asserts the property of bothbuilders — a test covering only the one I fixed would let the pair drift apart again, which is how
this arrived.
The
MessageBodyClassNamefilter that did exist on this path comes fromheader_filtersand onlyfires when the caller passes
message_class. That is why the issue's workaround ("always passmessage_classandsource") worked, and why it read as a caller obligation rather than a defect.Why the pin is a list, not the equality the report suggested
The report asked for
MessageBodyClassName = 'EnsLib.HL7.Message'. That is right for HL7 and wrongin general. The document class is declared as the
DOCCLASSclass parameter, and%Dictionary.CompiledClass.PrimarySuperis a~-delimited list that includes the class itself,so one
LIKEreturns the document class and every subclass of it in a single round trip, with noHL7 special case. Measured on IRIS for Health 2026.1 Build 235U:
EnsLib.HL7.SearchTableEnsLib.HL7.MessageEnsLib.EDI.X12.SearchTableEnsLib.EDI.X12.DocumentEnsLib.EDI.EDIFACT.SearchTableEnsLib.EDI.EDIFACT.DocumentEnsLib.XML.SearchTableEns.StreamContainerEns.MessageHeader(no DOCCLASS)The
EnsLib.XML.SearchTablerow is the reason: a bare equality would have droppedEns.MFT.StreamContainer,EnsLib.HTTP.GenericMessage,EnsLib.REST.GenericMessageandEnsLib.SOAP.GenericMessage— trading over-matching for under-matching, which is the worse of thetwo because the caller cannot see it. A body class that extends the document class is still a
document.
Two details that only a live instance tells you: the column is
_Default, notDefault(Defaultis reserved — the wrong name fails at runtime as SQLCODE -29, which no unit test would catch), and
the extent resolved from
Ens_Config.SearchTableProp.ClassExtentis the base extent, so acustom search table subclassing
EnsLib.HL7.SearchTableinherits the rightDOCCLASSwith noper-class registration.
An unresolvable document class is reported, not turned into a predicate
Ens.MessageHeaderhas noDOCCLASSand that query returns 0 rows rather than an error, so"cannot pin" is a state this code reaches. It emits the join unpinned — exactly the pre-change
behaviour — and adds a
warningnaming the extent, the namespace, why other classes can appear, andmessage_classas the way to constrain it. EmittingIN ()or a1=0instead would answer withzero rows, and zero rows from a search read as no message matched: the negative-fact shape CLAUDE.md
is about, and the defect this PR exists to remove, reintroduced one layer down.
an_unresolved_document_class_is_not_an_impossible_predicateasserts the absence of all five shapesthat would do that.
Verification
Read-only, against IRIS for Health 2026.1 on the verification instance (namespace APP):
DOCCLASSresolutions in the table above, including the 5-classfamily and the 0-row no-DOCCLASS case.
error.
Ens.MessageHeaderrows at all, so every row count in this namespace is zero and a zero provesnothing. Running the identical
JOIN … WHERE … IN ('…')form over dictionary tables that dohave rows: unpinned 7, pinned to one name 1, pinned to a 5-name family 1, pinned to a
name that matches nothing 0. The form filters, and a 5-name
INlist resolves.What is not verified, stated plainly: the row-level symptom itself — HL7 headers returned beside
colliding
Pkg.MSG.*ones — was not reproduced. It needs an HL7-indexed namespace with collidingbody IDs; the instance that has
EnsLib.HL7.*is read-only here, and the writable instances areCommunity edition with no HL7 at all (
EnsLib.HL7.Messageabsent). What is verified is the mechanismthe fix turns on, the validity of the SQL it emits, and that the predicate form filters rather than
being a no-op.
Mutation check — and the one that mattered
13 mutants, one per assertion plus a break of the sibling builder. 12 killed by the predicted
assertion on the first pass. One survived, and it was the important one.
Mutant 6 deleted the body-class join's pin outright — the exact defect this PR fixes, in the sibling
builder — and
neither_join_leaves_the_body_class_unconstrainedstayed green. The assertion wassql.contains("h.MessageBodyClassName"), andMessageBodyClassNameis one ofHEADER_COLS, whichboth builders prefix with
h.. So it was satisfied by the SELECT list and would have passed withthe filter gone entirely.
I had already hit that same trap earlier in this change, in
an_unresolved_document_class_is_not_an_impossible_predicate, and fixed it there — and did not carrythe fix to its sibling. Which is to say the parity test written to catch "one sibling got the guard"
was itself an instance of it. Both are now scoped to the WHERE clause, and the reason is recorded in
the test so the window is not widened again. Mutant 6 re-run: killed by
neither_join_leaves_the_body_class_unconstrained.Worth stating because it changes how much the rest of the suite is worth believing: a
contains()assertion on generated SQL is weaker than its name suggests whenever the string alsoappears in the projection. Every assertion here now names the clause it is about.
After the repair the file was restored byte-identical (sha checked), the post-restore baseline was
green, and the full gate was re-run from scratch rather than carried forward:
cargo fmt --checkrc=0, this file 12/12,
cargo clippy --workspace --all-targets -- -D warningsrc=0,cargo build --workspacerc=0, andcargo test --workspacewith the IRIS env unset rc=0 —1854 passed, 0 failed, 69 ignored across 66 binaries (66
test result:lines, so the count is ameasurement and not an empty grep).
Note for review
build_search_table_sql,build_body_join_sql,doc_class_family_sqlandunpinned_search_warningare nowpub. That matches the existing convention in this file —build_add_item_code,build_remove_item_code,build_set_settings_codeandbuild_message_body_codeare alreadypubfor the same reason — and it is what lets thesibling-parity assertion call both builders instead of grepping the source for a string, which
would pass on a comment.
The new assertions live in their own file rather than being appended to the
#[cfg(test)] modininterop.rs: measured over 13 PRs in this repo, every cross-branch conflict was an appended testmodule, and there are several open PRs touching this file.
🤖 Generated with Claude Code
https://claude.ai/code/session_01N7fLbq3ftb82ub45QCpsPB