Most Voyager tables have a discriminator column that tells you what kind of row you're looking at, and once you know that, every other column means the same thing on every row. DETAIL doesn't work that way. Two of its columns — HCHKORCHG and HINVORREC — don't have one meaning each. What they decode to depends entirely on which document class the row belongs to, and that document class isn't written down anywhere in DETAIL itself. You have to already know it, or derive it, before the row makes sense.
This is what "document class SQL" actually means on Voyager: the same physical column pair carries different semantics for an AR charge line than it does for an AP invoice line or a journal entry, and there's no flag on the row that spells that out for you.
What DETAIL actually is
DETAIL is transaction line-item and GL-distribution detail — the children of TRANS, linked back up through HINVORREC and HCHKORCHG. It's not a recurring-charge table (that's CAMRULE, the configuration table that generates billing lines before they ever post to TRANS) and its own ITYPE column is functionally unused — observed live, DETAIL.ITYPE only ever carries 0 or NULL. It isn't the discriminator. HCHKORCHG and HINVORREC, read together, are.
The side-decode
Both columns are, in a sense, foreign keys back to TRANS.HMY — but reading them correctly requires knowing that TRANS.HMY values are themselves typed by TRANS.iType (we've decoded that map separately). Cross-referencing the two columns' iType pairing against live data surfaces a consistent, document-class-specific pattern:
HCHKORCHG= an AR charge (iType 7),HINVORREC= a tenant receipt (iType 6) — a charge-to-receipt application line: this is how a payment gets matched against the charge it settles.HCHKORCHG= an AP payment (iType 2),HINVORREC= an AP invoice (iType 3) — the AP-side equivalent: a payment applied against the invoice it clears.HCHKORCHG=NULL,HINVORREC= a journal posting (iType 10) — journal-entry line items. These balance to zero within the entry, which is a useful sanity check if you're ever unsure you've isolated a full journal set.HCHKORCHG=HINVORREC= a tenant receipt (iType 6) — a receipt's own line items, not an application to something else. This is also the one combination wherehChargeCodeis actually populated at meaningful scale onDETAILrows.HCHKORCHG=NULL,HINVORREC= an AP invoice (iType 3) — invoice line items.- Combinations involving iType 15 (an adjustment/reversal document) show up as adjustment application lines, following the same active/target pattern as the rest.
The pattern underneath all of it: HCHKORCHG holds the "active" side of the transaction — the payment, the charge, or the receipt acting on something — and HINVORREC holds the "target" — the invoice, the receipt, or the journal entry being acted on or line-itemized. Neither column name tells you that on its own. You learn it by cross-referencing both columns' iType against the row.
The trap that follows from it
hChargeCode on DETAIL is not populated on application lines — the (7,6) charge-to-receipt rows, the (2,3) payment-to-invoice rows, none of it. If you're trying to classify what a posted charge actually was, joining DETAIL.hChargeCode to CHARGTYP returns nothing useful for the vast majority of rows. Charge classification lives one layer up, in CAMCHARG — the generated-billing-line table that sits between CAMRULE (the recurring-charge configuration) and the posted TRANS row. If you need to know what type of charge a transaction represents, go through CAMCHARG, not DETAIL.
This is the general shape of a document-class-side-decode: a column pair whose names suggest a single fixed join target, but whose actual target and meaning depend on a discriminator (here, the iType of the row each half points to) that lives on a different table entirely. It's the same underlying trap as the polymorphic hRecord/hCode pattern elsewhere in the schema, just expressed through a pair of columns instead of one.
Why this matters for the query you're about to write
Get the side-decode wrong and the query still runs. A join through DETAIL.hChargeCode returns an empty or near-empty result set instead of an error — it looks like the charge simply has no code, when really you're reading the wrong column for the document class you're in. A join that assumes HCHKORCHG always means "the charge" will misfire the moment it hits an AP-side row, where the same column position holds a payment instead.
The fix is the same discipline that shows up everywhere in Voyager's schema: identify the document class first — by the iType of what each half of the pair resolves to — and only then decide what the columns mean for that row. There's no shortcut that skips knowing the class up front.
Don't keep this in your head
This is exactly the kind of trap that doesn't show up in a data dictionary, because Voyager's own schema doesn't document it — it's only visible once you've cross-referenced live data and traced how the columns actually get populated per document type. PropETL's SQL Query Assistant carries this as verified knowledge: the DETAIL side-decode, the charge-billing architecture it connects to, and the TRANS.iType map it depends on, all queryable inside Claude instead of re-derived from scratch every time someone hits it.
It ships in every tier. Start the free 7-day trial — no credit card — and ask it to explain what a DETAIL row actually represents before you write the next join.
