API Flow - Code
Last updated: August 18, 2026
ASCII Flowchart
#L0 Stage^api
+-- #L1 BuildAttrsRequired^sapiBld
+-- #L1 BuildAttrsCache^sapiBld
+-- #L1 ResolveRecAttrs^apiRslv
| +-- #L2 ResolveAttr^apiRslv
+-- #L1 StageRecord^apiWFL
| +-- #L2 ResolveTargetEntity^apiRslv
| | +-- #L3 ResolveByEID^apiRslv
| | | +-- #L4 GetEID^apiGet
| | | +-- #L5 IsEID^utils
| | | +-- #L5 IsEntityRegistered^apiVld
| | | +-- #L5 ResolveLiveEID^apiRslv
| | +-- #L3 ResolveByNaturalKey^apiRslv
| | | +-- #L4 GetEID^apiGet
| | | +-- #L5 IsEID^utils
| | | +-- #L5 IsEntityRegistered^apiVld
| | | +-- #L5 ResolveLiveEID^apiRslv
| | | +-- #L4 GENID^utils
| | | +-- #L5 EPOCH_RAND^utils
| | +-- #L3 ResolveByUnique^apiRslv
| | +-- #L4 CountUniqueAttributes^apiRslv
| | | +-- #L5 GetUniqueMode^sapiGet
| | +-- #L4 GENID^utils
| | | +-- #L5 EPOCH_RAND^utils
| | +-- #L4 ResolveOwnership^apiRslv
| | | +-- #L5 GetUniqueMode^sapiGet
| | | +-- #L5 IsRangeValue^sapiVld
| | | +-- #L5 ResolveLiveEID^apiRslv
| | +-- #L4 ResolveDesignatedUnique^apiRslv
| | +-- #L5 GetEID^sapiGet
| | +-- #L5 GetUniqueMode^sapiGet
| | +-- #L5 ResolveOwnership^apiRslv
| | +-- #L6 GetUniqueMode^sapiGet
| | +-- #L6 IsRangeValue^sapiVld
| | +-- #L6 ResolveLiveEID^apiRslv
| | +-- #L6 GENID^utils
| | +-- #L7 - EPOCH_RAND^utils
| +-- #L2 IsEntityRegistered^apiVld
| +-- #L2 ValidateRecord^apiVld
| | +-- #L3 ValidateAttrPredicates^apiVld
| | +-- #L4 GetAttrValidFn^sapiGet
| | | +-- #L5 GetDictValue^sapiGet
| | +-- #L4 ValidateAttrPredVal^apiVld
| | +-- #L5 EscapeQuotes^apiVld
| +-- #L2 DatomAssert^apiWFL
| | +-- #L3 IsMultiValue^sapiVld
| | +-- #L3 DatomAssertMulti^apiWFL
| | | +-- #L4 DatomAssertMulti^apiWFL
| | | | [RECURSIVE]
| | | +-- #L4 DatomStageMulti^apiWFL
| | | +-- #L5 IsRefValType^sapiVld
| | | +-- #L5 ResolveAttrRef^sapiRslv
| | | | +-- #L6 ResolveIsaRef^sapiRslv
| | | | +-- #L6 ResolveKeyLabel^sapiRslv
| | | | | +-- #L7 - GetEID^apiGet
| | | | | +-- #L8 - IsEID^utils
| | | | | +-- #L8 - IsEntityRegistered^apiVld
| | | | | +-- #L8 - ResolveLiveEID^apiRslv
| | | | +-- #L6 ResolveRawEid^sapiRslv
| | | | +-- #L7 - GetEID^apiGet
| | | | +-- #L8 - IsEID^utils
| | | | +-- #L8 - IsEntityRegistered^apiVld
| | | | +-- #L8 - ResolveLiveEID^apiRslv
| | | +-- #L5 RegisterAttrVal^sapiRslv
| | | +-- #L6 IsNewRangeValue^sapiVld
| | | +-- #L6 CreateNewValKey^sapiRslv
| | | +-- #L6 IsRangeValue^sapiVld
| | | +-- #L6 GetValKey^sapiGet
| | +-- #L3 DatomAssertSingle^apiWFL
| +-- #L2 DatomRetract^apiWFL
| +-- #L3 IsMultiValue^sapiVld
| +-- #L3 DatomRetractMulti^apiWFL
| | +-- #L4 DatomRetractMulti^apiWFL
| | | [RECURSIVE]
| | +-- #L4 DatomStageMulti^apiWFL
| | +-- #L5 IsRefValType^sapiVld
| | +-- #L5 ResolveAttrRef^sapiRslv
| | | +-- #L6 ResolveIsaRef^sapiRslv
| | | +-- #L6 ResolveKeyLabel^sapiRslv
| | | | +-- #L7 - GetEID^apiGet
| | | | +-- #L8 - IsEID^utils
| | | | +-- #L8 - IsEntityRegistered^apiVld
| | | | +-- #L8 - ResolveLiveEID^apiRslv
| | | +-- #L6 ResolveRawEid^sapiRslv
| | | +-- #L7 - GetEID^apiGet
| | | +-- #L8 - IsEID^utils
| | | +-- #L8 - IsEntityRegistered^apiVld
| | | +-- #L8 - ResolveLiveEID^apiRslv
| | +-- #L5 RegisterAttrVal^sapiRslv
| | +-- #L6 IsNewRangeValue^sapiVld
| | +-- #L6 CreateNewValKey^sapiRslv
| | +-- #L6 IsRangeValue^sapiVld
| | +-- #L6 GetValKey^sapiGet
| +-- #L2 AddKeyword^apiWFL
| +-- #L2 DatomAssertSingle^apiWFL
+-- #L1 DatomRetract^apiWFL
+-- #L2 IsMultiValue^sapiVld
+-- #L2 DatomRetractMulti^apiWFL
+-- #L3 DatomRetractMulti^apiWFL
| [RECURSIVE]
+-- #L3 DatomStageMulti^apiWFL
+-- #L4 IsRefValType^sapiVld
+-- #L4 ResolveAttrRef^sapiRslv
| +-- #L5 ResolveIsaRef^sapiRslv
| +-- #L5 ResolveKeyLabel^sapiRslv
| | +-- #L6 GetEID^apiGet
| | +-- #L7 - IsEID^utils
| | +-- #L7 - IsEntityRegistered^apiVld
| | +-- #L7 - ResolveLiveEID^apiRslv
| +-- #L5 ResolveRawEid^sapiRslv
| +-- #L6 GetEID^apiGet
| +-- #L7 - IsEID^utils
| +-- #L7 - IsEntityRegistered^apiVld
| +-- #L7 - ResolveLiveEID^apiRslv
+-- #L4 RegisterAttrVal^sapiRslv
+-- #L5 IsNewRangeValue^sapiVld
+-- #L5 CreateNewValKey^sapiRslv
+-- #L5 IsRangeValue^sapiVld
+-- #L5 GetValKey^sapiGet
#L0 Transact^api
+-- #L1 TxAdd^sapiWFL
| +-- #L2 EPOCH^utils
+-- #L1 IsEntityRegistered^apiVld
+-- #L1 IsRefValType^sapiVld
#L0 PrintEntity^api
+-- #L1 GetEID^sapiGet
+-- #L1 BuildKeysCatalog^sapiBld
+-- #L1 GetEID^apiGet
| +-- #L2 IsEID^utils
| +-- #L2 IsEntityRegistered^apiVld
| +-- #L2 ResolveLiveEID^apiRslv
+-- #L1 IsMultiValue^sapiVld
+-- #L1 IsRefValType^sapiVld
+-- #L1 GetKeyForRefEID^apiGet
+-- #L1 GetDictValue^sapiGet
#L0 PrintHistory^api
+-- #L1 GetEID^sapiGet
+-- #L1 GetEID^apiGet
| +-- #L2 IsEID^utils
| +-- #L2 IsEntityRegistered^apiVld
| +-- #L2 ResolveLiveEID^apiRslv
L0 INIT^api
INIT
;;------------------------------------------------------------------
;; Procedure : INIT — Module entry point
;;
;; Call : DO INIT^api
;;
;; Purpose : Must be called once per user session before any other entry point in this module.
;;
;; Parameters: None
;;
;; Returns : Nothing (procedure)
;;
;; Sets : %ABR = 0 (process-level staging buffer)
;;
;; Notice : %ABR is initialised to 0 by INIT^api on session
;; startup and reset to 0 by TransactCLEAR after every
;; Transact call — success or failure.
;;------------------------------------------------------------------
KILL %ABR
SET %ABR=0
QUIT
L0 PE^api
PE(val,attr)
;;------------------------------------------------------------------
;; Routine : PE — Procedure
;; Call : DO PE^api(val,attr)
;;
;; Purpose : Short-form alias for PrintEntity.
;; Defaults val and attr at the call boundary so that
;; undefined actuals do not shadow PrintEntity's own
;; $GET handling with empty strings.
;;
;; Parameters
;; val : (IN, optional) Entity reference — eid, sys.attr.key,
;; or unique attribute value
;; attr : (IN, optional) Attribute key or aid, used to scope
;; unique-attribute resolution
;;------------------------------------------------------------------
SET val=$GET(val,"")
SET attr=$GET(attr,"")
D PrintEntity(val,attr)
QUIT
L0 PrintEntity^api
PrintEntity(val,attr)
;;------------------------------------------------------------------
;; Routine : PrintEntity — Procedure
;;
;; Purpose : Resolves an entity from any supported reference and
;; prints all currently active attribute values in tabular
;; format (eid | attribute | value).
;;
;; Only live datoms (latest tx with OP=1) are displayed.
;; Retracted datoms (OP=0) and empty resolved values are
;; suppressed.
;;
;; Cardinality-one attributes display only the most recent
;; active value.
;;
;; Cardinality-many attributes display all active values
;; (deduplicated by most recent transaction precedence).
;;
;; Accepts:
;; val : eid | sys.attr.key | unique attribute value
;; attr : optional aid (forces unique lookup attribute resolution path)
;;
;;------------------------------------------------------------------
NEW eid,aid,tx,valkey,op
NEW attrName,vk
NEW seen,isMany,done
NEW attrOrig
; Process attr parameter
SET attr=$GET(attr)
SET attrOrig=attr
; only resolve when attr is supplied AND not already numeric;
; -1 on failure is intentional — lets GetEID^apiGet attempt Path 2
; instead of silently falling back to Path 1
SET attr=$SELECT(attr="":attr,attr=+attr:attr,1:$$GetEID^sapiGet(attr))
DO BuildKeysCatalog^sapiBld
SET eid=$$GetEID^apiGet(val,attr)
IF eid=0 DO QUIT
. DO LOGINFO^logger("API","PrintEntity: entity not found — val=`"_val_"`")
; ------------------------------------------------------------------
; Header output
; ------------------------------------------------------------------
WRITE !
WRITE !,"======"
WRITE !,val
WRITE !,"======"
; ------------------------------------------------------------------
; Step 2 — iterate all attributes for this entity
; ^EATV(eid,aid,...) yields all attribute IDs in sorted order
; ------------------------------------------------------------------
SET aid=""
FOR SET aid=$ORDER(^EATV(eid,aid)) QUIT:aid="" DO
. NEW attrName,val,vk,isMany,done
. NEW seen
. ; Step 3 — resolve attribute display name
. SET attrName=""
. SET vk=$ORDER(%Keys(aid,AKEYID,""))
. IF vk'="" SET attrName=$GET(%Keys(aid,AKEYID,vk))
. ; Step 4 — determine cardinality behavior
. SET isMany=$$IsMultiValue^sapiVld(aid)
. KILL seen
. SET done=0
. ; --------------------------------------------------------------
. ; Step 5 — iterate transactions in descending order
. ; Most recent transaction is processed first
. ; --------------------------------------------------------------
. SET tx=$ORDER(^EATV(eid,aid,""),-1)
. FOR QUIT:(tx="")!(done) DO
. . NEW valkey,op
. . ; ----------------------------------------------------------
. . ; Step 6 — iterate all value keys within this transaction
. . ; ----------------------------------------------------------
. . SET valkey=$ORDER(^EATV(eid,aid,tx,""))
. . FOR QUIT:(valkey="")!(done) DO
. . . ; Step 6a — deduplicate across transactions - first encounter = most recent occurrence
. . . IF '$DATA(seen(valkey)) DO
. . . . SET seen(valkey)=1
. . . . ; Step 7 — check datom state (OP=1 live, OP=0 retracted)
. . . . SET op=$GET(^EATV(eid,aid,tx,valkey),0)
. . . . IF op DO
. . . . . ; Step 8 — resolve value key to human-readable label
. . . . . IF $$IsRefValType^sapiVld(aid) DO
. . . . . . NEW targeteid
. . . . . . SET targeteid=$GET(^TBD(aid,valkey))
. . . . . . SET val=$SELECT(targeteid'="":$$GetKeyForRefEID^apiGet(targeteid),1:"")
. . . . . ELSE DO
. . . . . . SET val=$$GetDictValue^sapiGet(aid,valkey)
. . . . . ; suppress unresolved/blank values
. . . . . IF val'="" DO
. . . . . . WRITE !
. . . . . . WRITE $JUSTIFY(eid,22)
. . . . . . WRITE " "
. . . . . . WRITE $EXTRACT(attrName_$JUSTIFY("",33),1,33)
. . . . . . WRITE " "
. . . . . . WRITE val
. . . . . ; Step 9 — cardinality-one optimization stop after first live value is found
. . . . . IF 'isMany SET done=1
. . . SET valkey=$ORDER(^EATV(eid,aid,tx,valkey))
. . SET tx=$ORDER(^EATV(eid,aid,tx),-1)
WRITE !
QUIT
L0 PH^api
PH(val,attr)
;;------------------------------------------------------------------
;; Routine : PH — Procedure
;; Call : DO PH^api(val,attr)
;;
;; Purpose : Short-form alias for PrintHistory.
;; Defaults val and attr at the call boundary so that
;; undefined actuals do not shadow PrintHistory's own
;; $GET handling with empty strings.
;;
;; Parameters
;; val : (IN, optional) Entity reference — eid, sys.attr.key,
;; or unique attribute value
;; attr : (IN, optional) Attribute key or aid, used to scope
;; unique-attribute resolution
;;------------------------------------------------------------------
SET val=$GET(val,"")
SET attr=$GET(attr,"")
D PrintHistory(val,attr)
QUIT
L0 PrintHistory^api
PrintHistory(val,attr)
;;------------------------------------------------------------------
;; Routine : PrintHistory — Procedure
;;
;; Purpose : Prints the raw ^EATV content for a resolved entity.
;; Displays full datom history including retractions.
;;
;; Accepts multiple entity reference forms via GetEID:
;; - eid
;; - sys.attr.key
;; - unique attribute value
;;
;; Parameters
;; val : (IN, optional) Entity reference — eid, sys.attr.key,
;; or unique attribute value
;; attr : (IN, optional) Attribute key or aid, used to scope
;; unique-attribute resolution
;;
;; Resolution Flow:
;; 1. Entity resolution val,attr -> GetEID -> eid
;; 2. Raw scan eid -> ^EATV(eid,aid,tx,valkey)
;;
;; Output:
;; One row per (eid, aid, tx, valkey, op)
;;
;; eid | aid | tx | valkey | ( + / - )
;;
;; Globals:
;; ^EATV - datom store
;;------------------------------------------------------------------
NEW eid,aid,tx,valkey,op
NEW attrOrig
; ------------------------------------------------------------------
; Step 0 — normalize attr into internal attribute id (aid)
; Supports:
; - numeric aid (e.g. 1005)
; - sys.attr.key (e.g. sandbox.dlc.id)
;
; Resolution is delegated to GetEID to avoid duplicate logic.
;
; attrOrig preserves the raw caller-supplied string BEFORE it is
; overwritten by the resolved/unresolved aid below. This is needed
; later — if resolution ultimately fails, the error branch must be
; able to tell whether the problem was an invalid ATTRIBUTE name
; (attrOrig was non-numeric and never resolved) versus an invalid
; VALUE under an otherwise-valid attribute.
; ------------------------------------------------------------------
SET attr=$GET(attr)
SET attrOrig=attr
IF attr'="" DO
. NEW tmp
. ; If attr is already numeric (aid), keep it
. IF attr=+attr QUIT
. ; Otherwise treat it as sys.attr.key and resolve via GetEID.
. ; Keep whatever GetEID^sapi returns, including -1 on failure,
. ; so GetEID^api receives a non-empty aid and attempts Path 2
. ; resolution rather than silently falling back to Path 1.
. SET tmp=$$GetEID^sapiGet(attr)
. SET attr=tmp
; ------------------------------------------------------------------
; Step 1 — resolve entity using unified resolver
; ------------------------------------------------------------------
SET eid=$$GetEID^apiGet(val,attr)
IF eid<0 DO QUIT
. NEW isEidShaped
. SET isEidShaped=$$IsEID^utils(val)
. ; Distinguish WHY resolution failed, for diagnosability.
. ; attrOrig is used (not attr) because attr has already been
. ; overwritten with the resolved aid (or -1) by Step 0.
. ;
. ; Case A — caller supplied a non-numeric attr string, but it
. ; never resolved to a known attribute id at all
. ; (typo, or not a registered sys.attr.key). Invalid
. ; ATTRIBUTE — caller/schema mistake.
. ;
. ; Case B — attr resolved successfully (or was passed in
. ; numerically), but val does not resolve as a unique
. ; value under that attribute. Invalid VALUE under a
. ; valid attribute.
. ;
. ; Case C — no attr supplied, and val is structurally a valid
. ; EID (correct length/format) but does not exist in
. ; ^EATV. This is different from a garden-variety
. ; unresolvable string — it means someone passed a
. ; real-looking entity ID that isn't actually live,
. ; e.g. stale, retracted-and-purged, or mistyped in a
. ; way that still happens to be well-formed.
. ;
. ; Case D — no attr supplied, val is not EID-shaped, and it
. ; also did not resolve via sys.attr.key. Ordinary
. ; "unrecognized reference" case.
. IF (attrOrig'="")&(attrOrig'=+attrOrig)&(attr<0) DO
. . DO LOGINFO^logger("API","PrintHistory: invalid attribute — `"_attrOrig_"` does not resolve to a known sys.attr.key")
. ELSE IF attr'="" DO
. . DO LOGINFO^logger("API","PrintHistory: invalid value — val=`"_val_"` not found under aid="_attr)
. ELSE IF isEidShaped DO
. . DO LOGINFO^logger("API","PrintHistory: invalid value — val=`"_val_"` has a valid EID structure but does not exist in ^EATV")
. ELSE DO
. . DO LOGINFO^logger("API","PrintHistory: invalid value — val=`"_val_"` not found via sys.attr.key lookup")
; ------------------------------------------------------------------
; Header
; ------------------------------------------------------------------
WRITE !
WRITE !,"======"
WRITE !,val
WRITE !,"======"
WRITE !
WRITE $JUSTIFY("eid",22)
WRITE " "
WRITE $JUSTIFY("aid",6)
WRITE " "
WRITE $JUSTIFY("tx",6)
WRITE " "
WRITE $JUSTIFY("valkey",14)
WRITE " "
WRITE "OP"
WRITE !
; ------------------------------------------------------------------
; Step 2 — iterate all attributes for this entity
; ------------------------------------------------------------------
SET aid=""
FOR SET aid=$ORDER(^EATV(eid,aid)) QUIT:aid="" DO
. NEW tx,valkey
. ; --------------------------------------------------------------
. ; Step 3 — iterate transactions (natural order preserved)
. ; --------------------------------------------------------------
. SET tx=""
. FOR SET tx=$ORDER(^EATV(eid,aid,tx)) QUIT:tx="" DO
. . NEW valkey
. . ; ----------------------------------------------------------
. . ; Step 4 — iterate valkeys for this tx
. . ; ----------------------------------------------------------
. . SET valkey=""
. . FOR SET valkey=$ORDER(^EATV(eid,aid,tx,valkey)) QUIT:valkey="" DO
. . . SET op=$GET(^EATV(eid,aid,tx,valkey),0)
. . . WRITE !
. . . WRITE $JUSTIFY(eid,22)
. . . WRITE " "
. . . WRITE $JUSTIFY(aid,6)
. . . WRITE " "
. . . WRITE $JUSTIFY(tx,6)
. . . WRITE " "
. . . WRITE $JUSTIFY(valkey,14)
. . . WRITE " "
. . . WRITE $SELECT(op=1:"( + )",1:"( - )")
WRITE !
QUIT
;; ========= END OF api.m =========
L0 AD^api
AD(ekv,akey,val,ns) ; Short-form alias for AssertDatom
;;------------------------------------------------------------------
;; Procedure : AD
;; Call : DO AD^api(ekv,akey,val,ns)
;;
;; Purpose : Short-form alias for AssertDatom.
;;------------------------------------------------------------------
DO AssertDatom^api($GET(ekv),$GET(akey),$GET(val),$GET(ns,"sandbox"))
QUIT
L0 AssertDatom^api
AssertDatom(ekv,akey,val,ns) ; Convenience wrapper of Stage to stage a single datom assertion for a new or existing entity
;;-------------------------------------------------------------------------------------------------------------------------
;; Procedure : AssertDatom
;; Call : DO AssertDatom^api(ekv,akey,val,ns)
;;
;; Purpose : Convenience wrapper of Stage to stage a single datom assertion for a new or existing entity.
;; Detects the ekv format, builds the minimal Stage-compatible Datom structure
;; and delegates to Stage.
;;
;; Parameters
;; ekv : (IN) entity specified with one of the following formats
;; Format A — raw EID e.g. "654f2564f8165hevmhe6"
;; Format B — compound lookup attr-key|value e.g. "sandbox.dlc.id|042"
;; Format C — entity's (sys.attr.key) e.g. "obj.tom_hanks"
;;
;; akey : (IN) attribute key e.g. "movie.title"
;;
;; val : (IN) Value to assert — "|"-delimited for multi-value
;;
;; ns : (IN, optional) Namespace. Default: "sandbox"
;;
;;
;; Examples : Format A — DO AssertDatom^api("6583d90d9b0e5704qj59","sys.attr.name","Mr. Foobar")
;; Format B — DO AssertDatom^api("sandbox.dlc.id|042","sandblox.dlc.checkup","F3 checkup update @en")
;; Format C — DO AssertDatom^api("obj.tom_hanks","sys.attr.name","Tommy")
;;
;;
;; TODO : ATTENTION FORMAT C !!! it can accept anything here as ekv
;; and if it is not found, it will create a new entity with
;; (1,"sys.attr.key")=ekv
;; (1,akey)=val
;;
;;--------------------------------------------------------------------------------------------------------------------
NEW Datom,attrkey,attrval
SET ns=$GET(ns,"sandbox")
IF $GET(ekv)="" DO LOGERROR^logger("API","AssertDatom: ekv is required") QUIT
IF $GET(akey)="" DO LOGERROR^logger("API","AssertDatom: akey is required") QUIT
IF $GET(val)="" DO LOGERROR^logger("API","AssertDatom: val is required") QUIT
IF $$IsEID^utils(ekv) DO ; Format A — raw EID
. SET Datom(1,"sys.attr.id")=ekv
ELSE IF ekv["|" DO ; Format B — compound lookup attr-key|value
. SET attrkey=$PIECE(ekv,"|",1)
. SET attrval=$PIECE(ekv,"|",2)
. SET Datom(1,attrkey)=attrval
ELSE SET Datom(1,"sys.attr.key")=ekv ; Format C — entity's sys.attr.key
SET Datom(1,akey)=val
DO Stage(.Datom,ns,0)
QUIT
L0 RD^api
RD(ekv,akey,val,ns) ; Short-form alias for RetractDatom
;;------------------------------------------------------------------
;; Procedure : RD
;; Call : DO RD^api(ekv,akey,val,ns)
;;
;; Purpose : Short-form alias for RetractDatom.
;;------------------------------------------------------------------
DO RetractDatom^api($GET(ekv),$GET(akey),$GET(val),$GET(ns,"sandbox"))
QUIT
L0 RetractDatom^api
RetractDatom(ekv,akey,val,ns) ; Convenience wrapper of Stage to stage a single datom retraction for an existing entity
;;---------------------------------------------------------------------------------------------------------------------
;; Procedure : RetractDatom
;; Call : DO RetractDatom^api(ekv,akey,val,ns)
;;
;; Purpose : Convenience wrapper of Stage to stage a single datom retraction for an existing entity.
;; Detects the ekv format, builds the minimal Stage-compatible Datom structure,
;; and delegates to Stage.
;;
;; Parameters
;; ekv : (IN) entity specified with one of the following formats
;; Format A — raw EID e.g. "654f2564f8165hevmhe6"
;; Format B — compound lookup attr-key|value e.g. "sandbox.dlc.id|042"
;; Format C — entity's (sys.attr.key) e.g. "obj.tom_hanks"
;;
;; akey : (IN) attribute key e.g. "movie.title"
;;
;; val : (IN) value to retract
;; if attribute is a single-valued then it is omitted (it is not optional)
;; if attribute is multi-valued then it must be present
;;
;; ns : (IN) optional namespace. Default: "sandbox"
;;
;;
;; Examples : Format A — DO RetractDatom^api("6583d90d9b0e5704qj59","sys.attr.name")
;; Format B — DO RetractDatom^api("sandbox.dlc.id|042","sandblox.dlc.checkup") — single-value atttribute
;; Format C — DO RetractDatom^api("obj.tom_hanks","sys.attr.name")
;;
;;
;; Notice : Retractions always target existing entities and must never create a new one
;; Format B and Format C both must check that entity already exists before Stage is called
;; If the entity cannot be resolved, the call is aborted with an error before any staging occurs.
;;---------------------------------------------------------------------------------------------------------------------
NEW Datom,attrkey,attrval,isEntityFound
SET isEntityFound=1
SET ns=$GET(ns,"sandbox")
IF $GET(ekv)="" DO LOGERROR^logger("API","RetractDatom: ekv is required") QUIT
IF $GET(akey)="" DO LOGERROR^logger("API","RetractDatom: akey is required") QUIT
IF $$IsEID^utils(ekv) DO ; Format A — raw EID
. SET isEntityFound=$$GetEID^apiGet(ekv)
. SET Datom(1,"sys.attr.id")=ekv
ELSE IF ekv["|" DO ; Format B — compound lookup attr-key|value
. SET attrkey=$PIECE(ekv,"|",1)
. SET attrval=$PIECE(ekv,"|",2)
. SET Datom(1,attrkey)=attrval
. SET isEntityFound=$$LookupEID^apiGet(attrval,attrkey)
ELSE DO ; Format C — entity's sys.attr.key
. SET isEntityFound=$$GetEID^apiGet(ekv)
. SET Datom(1,"sys.attr.key")=ekv
; Check if entity exists — Retractions must never create new entities.
IF isEntityFound DO
. SET Datom(1,akey,"-")=$GET(val) ; use $GET because for cardinality-one attributes val is omitted
. ; Delegate datoms to Stage
. DO Stage(.Datom,ns,0)
ELSE DO LOGERROR^logger("API","RetractDatom: entity not found — "_ekv)
QUIT
L0 Transact^api
Transact(user) ; Commits the staged %ABR buffer to the ABox globals under a single transaction.
;;----------------------------------------------------------------------------------------------------
;; Routine : Transact^api
;;
;; Purpose : Commits the staged %ABR buffer to the ABox globals under a single transaction.
;;
;; Parameters
;; user : (IN) User initiating the transaction, defaults to "Root" if not supplied.
;;
;;
;; Notice : All writes are wrapped in TSTART/TCOMMIT.
;; Any failure sets ok<1 and TROLLBACK discards all partial writes atomically.
;; : %ABR is always KILLed on exit — success or failure.
;;----------------------------------------------------------------------------------------------------
NEW ok ; transaction outcome:
; 2 no-op
; 1 success,
; 0 invalid,
; -1 runtime error (drives TransactEND dispatch)
NEW tx ; transaction stamp shared by every assertion/retraction in this batch
NEW aid ; current attribute id while walking staged %ABR
NEW valkey ; current value key while walking staged %ABR
NEW eid ; current entity id while walking staged %ABR
NEW cnt ; $INCREMENT() return value (tx / entity / datom counters) — used for its side effect only
SET ok=1
SET user=$GET(user,"Root")
; Validate — %ABR must be initialised and populated before commit.
IF '$DATA(%ABR) DO GOTO TransactEND
. SET ok=0
; Empty staging is a valid no-op, not an error.
IF $GET(%ABR,0)=0 DO GOTO TransactEND
. SET ok=2
; /\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
; Wrap all global writes in a single transaction
TSTART ()
NEW $ETRAP
SET $ETRAP="GOTO TransactERR^api"
; Generate transaction ID now — all assertions and retractions in this batch share a single
; tx stamp. Increment ^TXE root counter to track total indexed transactions.
SET tx=$$TxAdd^sapiWFL(user)
SET cnt=$INCREMENT(^TXE)
; Iterate all staged entity records. For each entity:
SET eid=""
FOR SET eid=$ORDER(%ABR(eid)) QUIT:(eid="")!(ok<1) DO
. QUIT:eid=%ABR ; skip root node counter %ABR=n — not an entity subscript
. IF '$$IsEntityRegistered^apiVld(eid) DO ; Register entity in ABox entity index if not yet asserted
. . SET ^ABE(eid,1)="" ; mark entity as asserted
. . SET cnt=$INCREMENT(^ABE) ; increment total ABox entity counter
. ; Iterate all attributes for this entity
. SET aid=""
. FOR SET aid=$ORDER(%ABR(eid,aid)) QUIT:(aid="")!(ok<1) DO
. . ; Iterate all value keys for this attribute
. . SET valkey=""
. . FOR SET valkey=$ORDER(%ABR(eid,aid,valkey)) QUIT:(valkey="")!(ok<1) DO
. . . NEW op
. . . SET op=%ABR(eid,aid,valkey) ; Read OP from staging buffer
. . . SET ^EATV(eid,aid,tx,valkey)=op ; primary datom store, everything about a given entity
. . . SET ^AVET(aid,valkey,eid,tx)=op ; secondary-lookup index to find entities by attribute + value
. . . SET ^AEVT(aid,eid,valkey,tx)=op ; all values for a given attribute, column access
. . . IF $$IsRefValType^sapiVld(aid) DO
. . . . SET ^VAET(valkey,aid,eid,tx)=op ; reverse-reference index (reference attributes only)
. . . SET ^TXE(tx,eid)="" ; transaction → entity index
. . . SET cnt=$INCREMENT(^EATV) ; increment datom counter
. . . DO LOGDEBUG^logger("API","Transact: ^EATV("_eid_","_aid_","_tx_","_valkey_")="_op)
TransactEND ; dispatch on ok/$TLEVEL to commit, rollback, or log-only
;;----------------------------------------------------------------------------------------------
;; Exit codes for ok:
;; 2 : no-op — no datom was asserted in the database, no changes, no entities affected
;; 1 : normal completion — TCOMMIT persists all globals
;; 0 : invalid data — validation guard failed, TROLLBACK discards partial writes
;; -1 : runtime error — $ETRAP redirected here via TransactERR, TROLLBACK discards all writes
;;----------------------------------------------------------------------------------------------
IF (ok=1)&($TLEVEL>0) DO GOTO TransactCLEAR
. TCOMMIT
. DO LOGINFO^logger("API","Transact: committed "_$GET(%ABR,0)_" records, tx="_tx)
IF (ok=0)&($TLEVEL>0) DO GOTO TransactCLEAR
. DO LOGERROR^logger("API","Transact: rolling back — invalid staging data")
. TROLLBACK
IF (ok=-1)&($TLEVEL>0) DO GOTO TransactCLEAR
. DO LOGCRITICAL^logger("API","Transact: rolling back — runtime error")
. TROLLBACK
; No transaction was ever open — this is the pre-TSTART guard-clause path
; (%ABR missing). Nothing to roll back, just log and clear.
IF (ok=0)&($TLEVEL=0) DO GOTO TransactCLEAR
. DO LOGERROR^logger("API","Transact: %ABR not initialised — call INIT^api first")
; No transaction was ever open — this is the pre-TSTART guard-clause path
; (%ABR empty). Nothing to commit or roll back, just log and clear.
IF (ok=2)&($TLEVEL=0) DO GOTO TransactCLEAR
. DO LOGINFO^logger("API","Transact: no-op — no staged records, nothing committed")
TransactCLEAR ; reset staging buffer for the next batch, regardless of outcome
KILL %ABR ; always clear — success or failure
SET %ABR=0
QUIT
TransactERR ; error trap target — captures runtime errors during the write loop
; Restore caller's trap immediately to prevent re-entry into this handler on any nested error
NEW $ETRAP
SET ok=-1
DO LOGCRITICAL^logger("API","Transact: unexpected runtime error: "_$ZSTATUS)
SET $ECODE="" ; clear error code — required or error propagates
GOTO TransactEND
; /\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
L1 TxAdd^sapiWFL
TxAdd(user)
;;------------------------------------------------------------------
;; Function : TxAdd^sapiWFL
;; Purpose : Creates and registers a new transaction record in the
;; ^TX global, returning its unique transaction ID.
;; Initialises the ^TX counter on first use.
;;
;; Scope
;; Reads : ^TX — transaction counter
;; Writes : ^TX — incremented transaction counter
;; ^TX(id,"dt") — epoch timestamp of transaction
;; ^TX(id,"user") — user who initiated the transaction
;;
;; Parameters
;; user : (IN) String. The user initiating the transaction.
;;
;; Returns : id — Integer. The newly assigned transaction ID.
;;
;; Call : SET tx=$$TxAdd(user)
;;
;; Usage
;; SET tx=$$TxAdd("Root")
;;
;;------------------------------------------------------------------
NEW id,epoch
; Initialize counter if not set, starting counter at 7399
IF '$DATA(^TX) SET ^TX=7399
SET id=$INCREMENT(^TX)
SET epoch=$$EPOCH^utils
SET ^TX(id,"dt")=epoch
SET ^TX(id,"usr")=user
QUIT id
L2 EPOCH^utils
EPOCH()
;;------------------------------------------------------------------
;; Routine : EPOCH Function
;;
;; Call : WRITE $$EPOCH^<routine>
;;
;; Purpose :
;; Generates a hexadecimal representation of the current
;; Unix/Universal timestamp in microseconds.
;;
;; The timestamp is converted from decimal to hexadecimal,
;; transformed to lowercase, and returned as a unique
;; 13-character string.
;;
;; Returns :
;; A lowercase hexadecimal string representing the current
;; timestamp in microseconds.
;;
;; Notice :
;; - Uses YottaDB special variable $ZUT.
;;------------------------------------------------------------------
NEW HEXUS
; Convert the Unix/Universal timestamp in microseconds to a 13-character Hexadecimal string
SET HEXUS=$ZCONVERT($ZUT,"dec","hex")
; Convert it to lower case and return it
QUIT $ZCONVERT(HEXUS,"L")
L1 IsRefValType^sapiVld
IsRefValType(aid)
;;------------------------------------------------------------------
;; Function : IsRefValType^sapiVld
;; Call : IF $$IsRefValType^sapiVld(aid)
;;
;; Purpose : Returns 1 if the metadata attribute is reference-typed, 0 otherwise.
;; A reference data type stores entity IDs as values
;;
;;
;; Returns :
;; 1 — reference data type (values are entity IDs)
;; 0 — literal type (values are raw strings)
;;
;; Notes :
;; ''$DATA(...) double-negation canonicalize $DATA's four-valued return into a strict 0/1 boolean.
;;------------------------------------------------------------------
NEW isRef
SET isRef=''$DATA(^TBAVET(RANGEID,REFKEY,aid))
;IF 'isRef DO LOGDEBUG^logger("sapiVld","IsRefValType: not reference-typed — aid="_aid)
QUIT isRef
L0 Stage^api
Stage(Data,ns,isBulkLoad,ok) ; builds a staged set of datoms (all or nothing) that will later be consumed by Transact.
;;------------------------------------------------------------------------------------------------------------------------------
;; Routine : Stage
;;
;; Call : DO Stage^api(.Data,ns,isBulkLoad,.ok)
;;
;; Usage : Entry point for staging multiple records into %ABR before
;; transactional commit via Transact^api. Supports both assertions
;; (default) and retractions ("-") within the same record.
;;
;; Purpose : Resolves each record's attribute keys, orchestrates entity
;; resolution/validation/staging via StageRecord, and guarantees
;; batch atomicity — the FOR loop stops on the first failure, and
;; %ABR is discarded in full, not just the failing record.
;;
;; Parameters
;; Data : (IN) Multi-record input structure
;; Data(idx,attributeName) = value ; assert (default)
;; Data(idx,attributeName,"-") = value ; retract (explicit)
;;
;; ns : (IN) namespace prefix path of arbitrary depth (one or more dot-separated segments)
;; It's not restricted to a single top-level segment;
;; it's simply whatever comes before the entity-type-qualified attribute name
;; e.g. (john.sandbox.cinema.) --> ns
;; (movie.title) --> akey
;; Entity's attributes are relative to this namespace. e.g. movie.title
;;
;; isBulkLoad : (IN) 1 = skips record validation
;; 0 = performs record validation
;;
;; ok : (OUT) 2 : every record staged cleanly, but nothing changed
;; 1 : if every record staged;
;; 0 : validation/resolution failure
;; -1 : deeper failure surfaced the batch was aborted %ABR discarded
;;
;; Namespace convention:
;; ns parameter is the schema namespace used for attribute resolution and validation.
;;
;; Data Input Example : e.g. ns="john.sandbox.cinema"
;; Then input records use attributes relative to the schema namespace e.g.:
;; S M(17,"movie.title")="Seven @en"
;; S M(17,"movie.genre")="thriller|mystery"
;; etc...
;; Do NOT pass fully qualified attribute names when using Stage:
;; S M(17,"john.sandbox.cinema.movie.imdbid")="tt0011100" <-- invalid attribute
;;
;; %ABR Example : Staged triplets of two records
;; %ABR("6583f05ec5dd43kbzqc3",1000,"K3E8.01889C")=1
;; %ABR("6583f05ec5dd43kbzqc3",1001,"K3E9.018859")=1
;; %ABR("6583f05ec5dd43kbzqc3",1001,"K3E9.01885A")=1
;; %ABR("6583f05ec5dd43kbzqc3",1002,"K3EA.018880")=1
;;
;; %ABR("6583f05ec5e850wgrgnc",1000,"K3E8.01889E")=1
;; %ABR("6583f05ec5e850wgrgnc",1001,"K3E9.018876")=1
;; %ABR("6583f05ec5e850wgrgnc",1002,"K3EA.01889F")=1
;;
;;------------------------------------------------------------------------------------------
SET ok=1
; Guard — %ABR must exist before staging can begin
IF '$DATA(%ABR) DO LOGERROR^logger("API","Stage: %ABR not initialised — call INIT^api first") SET ok=0 QUIT
NEW idx ; drives the FOR loop over record indices in Data
NEW ReqAttr ; required-attribute map for ns, built once for the whole batch
NEW AttrCache ; forward/reverse attribute key cache for ns, built once for the whole batch
NEW recCount ; total records attempted, for the summary log line
NEW failedIdx ; idx of the record that caused the batch to abort, if any
SET ns=$GET(ns,"sandbox") ; default namespace when caller omits ns
SET isBulkLoad=$GET(isBulkLoad,0) ; default 0 — validation runs unless caller opts into bulk load
SET recCount=0 ; running count of records attempted, for the summary log line
SET failedIdx="" ; idx of the record that aborted the batch, if any
; Validate — %ABR must be initialised and populated before staging phase starts.
IF '$DATA(%ABR) DO QUIT
. DO LOGERROR^logger("API","Transact: %ABR not initialised — call INIT^api first")
; Build required attributes map — used by ValidateRecord
DO BuildAttrsRequired^sapiBld(ns,.ReqAttr)
; Build schema attributes cache —
; forward map used by ResolveRecAttrs
; reverse map used by ValidateRecord
DO BuildAttrsCache^sapiBld(ns,.AttrCache)
SET idx=""
; Iterate the top-level first subscript of Data input. Each idx represents a temporary ID for a record.
; Loop exits when idx is exhausted, or stops immediately on first failure (ok<1) —
; remaining records are never attempted.
FOR SET idx=$ORDER(Data(idx)) QUIT:(idx="")!(ok<1) DO
. NEW ResolvedRec ; current record's attributes resolved to internal aids
. SET recCount=recCount+1
. SET ok=$$ResolveRecAttrs^apiRslv(ns,$NAME(Data(idx)),.AttrCache,.ResolvedRec) ; Resolve attribute keys to internal aids
. IF ok=0 DO QUIT
. . SET failedIdx=idx
. . DO LOGERROR^logger("API","Stage: attribute resolution failed for idx="_idx_" — batch will be aborted")
. SET ok=$$StageRecord^apiWFL(.ResolvedRec,isBulkLoad,.ReqAttr,.AttrCache)
. IF ok<1 SET failedIdx=idx
IF ok<1 DO
. KILL %ABR
. SET %ABR=0
. DO LOGERROR^logger("API","Stage: batch aborted at record idx="_failedIdx_" (ok="_ok_") — entire batch discarded, "_recCount_" record(s) attempted for ns="_ns)
ELSE IF $GET(%ABR,0)=0 DO
. SET ok=2
. DO LOGINFO^logger("API","Stage: no-op — no staged records, nothing committed")
ELSE DO
. DO LOGINFO^logger("API","Stage: processed "_recCount_" record(s) — "_$GET(%ABR,0)_" record(s) staged for ns="_ns_" isBulkLoad="_isBulkLoad)
QUIT
L1 BuildAttrsRequired^sapiBld
BuildAttrsRequired(ns,ReqAttr) ; collect required attribute IDs for a schema prefix
;;------------------------------------------------------------------
;; Routine : BuildAttrsRequired^sapiBld
;;
;; Call : DO BuildAttrsRequired^sapiBld(ns,.ReqAttr)
;;
;; Usage : Called before staging a record to know which attributes must
;; be present, then checked against the caller's own data
;;
;; Purpose : Populates ReqAttr with the attribute IDs of all required
;; attributes for the given schema prefix.
;;
;; Parameters
;; ns : (IN) Schema prefix string, e.g. "sandbox.movie"
;; ReqAttr : (OUT) Local array, passed by reference
;;
;; Scope (Indirect Globals Access)
;; Reads : %Schema — populated internally via BuildSchemaView
;;------------------------------------------------------------------
; Output:
; ReqAttr = total count of required attributes
; ReqAttr(aid) = attribute's own key string (from sys.attr.key),
; present only for attributes flagged sys.attr.required=1
KILL ReqAttr
NEW aid ; iterates every attribute in %Schema
SET ReqAttr=0
DO BuildSchemaView(ns)
SET aid=""
; Keep only attributes flagged required, storing their key string for lookup
FOR SET aid=$ORDER(%Schema(aid)) QUIT:aid="" DO
. IF $GET(%Schema(aid,"sys.attr.required"))=1 DO
. . SET ReqAttr(aid)=$GET(%Schema(aid,"sys.attr.key"))
. . SET ReqAttr=$INCREMENT(ReqAttr)
QUIT
L1 BuildAttrsCache^sapiBld
BuildAttrsCache(ns,AttrsCache) ; build forward/reverse attribute caches for a namespace
;;------------------------------------------------------------------
;; Routine : BuildAttrsCache^sapiBld
;;
;; Call : DO BuildAttrsCache^sapiBld(ns,.AttrsCache)
;;
;; Usage : DO BuildAttrsCache^sapiBld("sandbox",.AttrsCache)
;;
;; Purpose : Builds a cache of schema attributes belonging to the specified
;; namespace, always including the shared sys.attr namespace.
;;
;; Parameters
;; ns : (IN) Namespace prefix, e.g. "sandbox" or "sandbox.movie"
;; AttrsCache : (OUT) Local array, passed by reference
;;
;; Scope
;; Reads : ^TBDR(AKEYID,fullkey)
;;------------------------------------------------------------------
; Two entries are written per attribute:
; AttrsCache(fullkey) = aid — forward map, used by ResolveRecAttrs
; AttrsCache(aid) = sub-namespace — reverse map, used by ValidateRecord
; to check namespace membership without
; a schema lookup per attribute
;
; Example:
; AttrsCache("sandbox.movie.genre") = 700
; AttrsCache("sandbox.movie.imdbid") = 703
; AttrsCache("sys.attr.key") = 210
; AttrsCache(700) = "sandbox.movie"
; AttrsCache(703) = "sandbox.movie"
; AttrsCache(210) = "sys.attr"
NEW fullkey ; iterates registered attribute key strings in ^TBDR(AKEYID,*)
NEW prefix ; current namespace + "." being matched against fullkey
NEW nsIdx ; iterates nsList
NEW nsList ; the two namespaces to scan: ns and the always-included sys.attr
NEW aid ; resolved attribute entity ID for fullkey
NEW nsDepth ; number of dot-separated segments in the current namespace,
; used so the reverse map keeps exactly that many segments of fullkey
KILL AttrsCache
; Always include sys.attr namespace in addition to the requested namespace
SET nsList(1)=ns
SET nsList(2)="sys.attr"
; Iterate over each namespace in nsList
SET nsIdx=""
FOR SET nsIdx=$ORDER(nsList(nsIdx)) QUIT:nsIdx="" DO
. SET prefix=nsList(nsIdx)_"."
. SET nsDepth=$LENGTH(nsList(nsIdx),".") ; e.g. "sandbox.movie" -> 2, "sandbox" -> 1
. SET fullkey=""
. ; Iterate over all registered attribute keys matching this namespace prefix
. FOR SET fullkey=$ORDER(^TBDR(AKEYID,fullkey)) QUIT:fullkey="" DO
. . IF $EXTRACT(fullkey,1,$LENGTH(prefix))'=prefix QUIT ; outside this namespace — skip
. . SET aid=$$GetEID^sapiGet(fullkey)
. . SET AttrsCache(fullkey)=aid ; forward map: key -> aid
. . ; Reverse map: aid -> sub-namespace.
. . ; sys.attr is flat — attribute names sit directly under sys.attr with
. . ; no entity-type level in between (e.g. "sys.attr.key"), so nothing
. . ; is stripped and the full fullkey is kept as-is.
. . ; Every other namespace (e.g. "sandbox") has an entity-type segment
. . ; between the namespace and the attribute name (e.g. "sandbox.movie.genre"),
. . ; so the trailing attribute-name segment is stripped via $PIECE,
. . ; leaving the entity-type-qualified sub-namespace (e.g. "sandbox.movie").
. . SET AttrsCache(aid)=$SELECT(nsList(nsIdx)="sys.attr":fullkey,1:$PIECE(fullkey,".",1,nsDepth))
QUIT
L1 ResolveRecAttrs^apiRslv
ResolveRecAttrs(ns,dataRef,AttrCache,ResolvedRec) ; Resolve a single record's attribute names into internal attribute IDs building ResolvedRec(op,aid)=value
;; ------------------------------------------------------------------------------------------------------------------
;; Function : $$ResolveRecAttrs^apiRslv
;;
;; Purpose: Resolve a single record's attribute names into internal attribute IDs
;; building ResolvedRec(op,aid)=value
;;
;; Parameters:
;; ns : (IN) Namespace prefix path of arbitrary depth
;;
;; dataRef : (IN) Named reference e.g. "Data(17)"
;; that gives access to record No.17 in local array Data
;;
;; AttrCache : (IN) Attribute resolution cache, read-only here:
;; AttrCache("sandbox.movie.title")=700
;; AttrCache("sandbox.movie.genre")=701
;;
;; ResolvedRec : (IN/OUT) Starts empty, then nodes are stored - ResolvedRec(op,attributeId)=value
;;
;; Example:
;; Data(1,"movie.title")="The Reader"
;; Data(1,"movie.releaseYear")=2008
;; Data(1,"movie.genre","-")="thriller"
;;
;; becomes
;; ResolvedRec("+",700)="The Reader"
;; ResolvedRec("+",702)=2008
;; ResolvedRec("-",701)="thriller"
;;
;; Returns:
;; 1 - if every attribute name resolved
;; 0 - if any attribute could not be resolved
;; ------------------------------------------------------------
NEW akey,aid,ok
SET akey=""
SET ok=1
; Iterate the attribute-name subscripts of dataRef; resolve each to an aid via $$ResolveAttr
; Loop exits when akey is exhausted, or stops immediately on the first unresolvable attribute (ok=0)
; remaining attributes are never attempted.
FOR SET akey=$ORDER(@dataRef@(akey)) QUIT:(akey="")!(ok=0) DO
. SET aid=$$ResolveAttr(ns,akey,.AttrCache)
. IF aid=0 SET ok=0 QUIT
. IF $DATA(@dataRef@(akey))#10 SET ResolvedRec("+",aid)=@dataRef@(akey)
. IF $DATA(@dataRef@(akey,"-")) SET ResolvedRec("-",aid)=$GET(@dataRef@(akey,"-"))
QUIT ok
L2 ResolveAttr^apiRslv
ResolveAttr(ns,akey,AttrCache) ; Resolve a single attribute key to its internal attribute ID
;; ----------------------------------------------------------------------------------------------
;; Procedure : $$ResolveAttr^apiRslv
;;
;; Purpose: Resolve a single attribute name to its internal attribute ID,
;; trying the namespace prefixed key first, then try with a bear key
;;
;; Inputs:
;; ns - Namespace prefix path of arbitrary depth
;; akey - Attribute name to resolve (e.g. "movie.title").
;; AttrCache - Attributes resolution cache (see ResolveRecAttrs).
;;
;; Output:
;; Returns aid - internal attribute ID (any number > 0), or 0 if unresolvable.
;; ----------------------------------------------------------------------------------------------
NEW aid
; +$GET(...) coersion +""=0
SET aid=+$GET(AttrCache(ns_"."_akey)) ; try ns prefixed key e.g. sandbox. movie.genre --> 700
IF aid=0 SET aid=+$GET(AttrCache(akey)) ; try with a bear key e.g. sys.attr.id --> 277
; both attribute resolution methods failed
IF aid=0 DO LOGERROR^logger("apiRslv","ResolveRecAttrs: unresolvable attribute name '"_akey_"' for ns="_ns_" — aborting record resolution")
QUIT aid
L1 StageRecord^apiWFL
StageRecord(ResolvedRec,isBulkLoad,ReqAttr,AttrCache) ; resolve, validate, and stage one record's triplets into %ABR
;;-------------------------------------------------------------------------------------------------
;; Function : $$StageRecord^apiWFL
;;
;; Purpose : Resolve target entity, validate, and stage one record's assertions and
;; retractions into %ABR.
;;
;; At the staging phase, ResolvedRec contains ONLY entity facts — control
;; attributes have been consumed by ResolveTargetEntity and eid is immutable.
;; Assertions "+" are processed before retractions "-" to ensure entity key is
;; staged before any retractions are attempted.
;;
;; Parameters
;; ResolvedRec : (IN) Resolved record — assertions and retractions; mutated by
;; ResolveTargetEntity during entity-key resolution
;; isBulkLoad : (IN) If 1, skips ValidateRecord entirely
;; ReqAttr : (IN) Required attributes map from BuildAttrsRequired
;; AttrCache : (IN) Schema attribute cache from BuildAttrsCache
;;
;; Returns
;; 2 — there have been no changes on the record no-op
;; 1 — this record staged cleanly (see hasChanges handling below)
;; 0 — controlled failure (resolution/validation/staging rejected the record)
;; -1 — uncaught runtime error
;;
;; Notes
;; DatomAssert/DatomRetract may return 1 (staged), 2 (no-op), or 0 (failed) per attribute.
;; Only 0 stops the staging loops early. A record where staging fully completes — whether
;; every attribute wrote something, or some/all were no-ops — is reported as result=1;
;; hasChanges (via $DATA(%ABR(eid))) is what actually distinguishes "wrote something" from
;; "processed cleanly but nothing changed," so the per-attribute 1-vs-2 distinction doesn't
;; need to be tracked separately here.
;;------------------------------------------------------------------------------------------------------
NEW eid ; target entity being asserted or retracted against
NEW isNewEntity ; whether the target entity is new or previously registered
NEW hasChanges ; whether staging produced an actual change to %ABR
NEW result ; outcome of this record's processing, returned to caller
SET eid=0
SET result=1
SET hasChanges=0
; Phase 1 — Resolve target entity. No transaction open yet — QUIT directly on failure.
SET eid=$$ResolveTargetEntity^apiRslv(.ResolvedRec) ; error or success message is logged in ResolveTargetEntity help functions
IF eid=0 QUIT 0 ; aborting — entity resolution failed
; Phase 2 — Validate entity facts. Still no transaction open — QUIT directly on failure.
SET isNewEntity='$$IsEntityRegistered^apiVld(eid)
IF 'isBulkLoad DO
. SET result=$$ValidateRecord^apiVld(.ResolvedRec,.ReqAttr,.AttrCache,isNewEntity)
ELSE DO
. DO LOGWARNING^logger("apiWFL","StageRecord: isBulkLoad=1 — ValidateRecord skipped for eid="_eid_" (required-attribute checks bypassed)")
IF 'result DO QUIT 0
. DO LOGERROR^logger("apiWFL","StageRecord: aborting — entity fact validation failed")
; /\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
; Phase 3 — Stage datoms into %ABR (local only — no global write, no TSTART needed).
; $ETRAP armed to contain a runtime error inside this phase without aborting Stage^api's loop.
NEW $ETRAP
SET $ETRAP="GOTO StageRecordERR^apiWFL"
NEW aid ; current attribute id while walking ResolvedRec's staged assertions/retractions
NEW ok ; per-attribute staging outcome — loop stops early only when this is 0
SET ok=1
; Process assertions "+" first
SET aid=""
FOR SET aid=$ORDER(ResolvedRec("+",aid)) QUIT:aid=""!'ok DO
. SET ok=$$DatomAssert(eid,aid,ResolvedRec("+",aid))
; Process retractions "-" — only if assertions succeeded
IF ok DO
. SET aid=""
. FOR SET aid=$ORDER(ResolvedRec("-",aid)) QUIT:aid=""!'ok DO
. . SET ok=$$DatomRetract(eid,aid,ResolvedRec("-",aid))
SET hasChanges=$DATA(%ABR(eid))>0
IF ok=0 SET result=0
ELSE IF hasChanges SET result=1
ELSE SET result=2
GOTO StageRecordEND
L2 ResolveTargetEntity^apiRslv
ResolveTargetEntity(ResolvedRec) ; Resolve target entity ID for the data record
;;------------------------------------------------------------------
;; Function : ResolveTargetEntity^apiRslv
;;
;; Purpose : Resolve target entity ID for the data record
;;
;; Parameters
;; ResolvedRec : (IN/OUT) Resolved record — control attributes consumed
;;
;; Returns :
;; eid — resolved target entity ID on success
;; 0 — any resolution failure
;;------------------------------------------------------------------
NEW eid ; target entity id
SET eid=0
; Path 0 — sys.attr.id (explicit raw EID)
IF $DATA(ResolvedRec("+",SYSATTRID)) SET eid=$$ResolveByEID(.ResolvedRec)
; Path 1 — sys.attr.key (bare key)
ELSE IF $DATA(ResolvedRec("+",AKEYID)) SET eid=$$ResolveByNaturalKey(.ResolvedRec)
; Path 2 — unique attribute resolution
ELSE SET eid=$$ResolveByUnique(.ResolvedRec)
QUIT eid
L3 ResolveByEID^apiRslv
ResolveByEID(ResolvedRec) ; resolves target entity via explicit raw EID reference using sys.attr.id
;;--------------------------------------------------------------------
;; Function : $$ResolveByEID^apiRslv
;;
;; Purpose : ResolveTargeEntity/Path 0 — explicit raw EID reference.
;; Resolves the target entity using the value of sys.attr.id attribute
;;
;; Parameters
;; ResolvedRec : (IN/OUT) Resolved record — sys.attr.id node is killed
;;
;; Returns :
;; eid resolved entity ID
;; 0 failed to resolve
;;------------------------------------------------------------------
NEW idval ; raw entity ID value from ResolvedRec
NEW eid ; resolved entity ID, or 0 on failure
SET idval=$GET(ResolvedRec("+",SYSATTRID))
; remove sys.attr.id node ResolvedRec node — it must not reach DatomAssert as entity fact
KILL ResolvedRec("+",SYSATTRID)
; Validates both format (structurally valid EID) and existence (must exist in ^EATV)
; $$GetEID logs both succes and failure messages
SET eid=$$GetEID^apiGet(idval)
QUIT eid
L4 GetEID^apiGet
GetEID(val,aid)
;;------------------------------------------------------------------
;; Function : GetEID^apiGet
;;
;; Call : SET eid=$$GetEID^apiGet(val)
;; SET eid=$$GetEID^apiGet(val,aid)
;;
;; Purpose : Resolves any supported entity reference to an internal entity ID (eid).
;;
;; Three resolution paths are attempted in order:
;; Path 0 — Strict EID validation + existence check (aid omitted)
;; Path 1 — sys.attr.key lookup (aid omitted)
;; Path 2 — UNQINSERT/UNQUPSERT attribute lookup (aid supplied)
;;
;; Parameters
;; val : (IN) One of:
;; - persisted eid e.g. "654f2564f8165hevmhe6"
;; - sys.attr.key value e.g. "obj.tom_hanks"
;; - unique attr value e.g. "042"
;;
;; aid : (IN) aid is required only for path 2.
;;
;; Returns :
;; eid : resolved entity ID
;; 0 : cannot be resolved
;;
;; Notice : LIVENESS
;; Both Path 1 and Path 2 walk ALL eid candidates under ^AVET(aid,valkey,candidate)
;; and accept only the one for which GetCurrentVK(candidate,aid)=valkey
;; i.e. the one whose most recent tx for `aid` is still asserting this exact `valkey`.
;;------------------------------------------------------------------
; Guard — empty val can never resolve via any path; fail fast before touching globals.
IF val="" DO QUIT 0
. DO LOGERROR^logger("apiGet","GetEID: empty value supplied — cannot resolve")
NEW eid,isEidShaped
SET eid=0
SET aid=$GET(aid)
; Guard — aid, if supplied, must be a numeric attribute ID, not a sys.attr.key string.
IF aid'="",aid'=+aid DO QUIT 0
. DO LOGERROR^logger("apiGet","GetEID: aid must be numeric — got '"_aid_"'. Use LookupEID^api if resolving by a sys.attr.key string.")
; ------------------------------------------------------------------
; Path 0 — Strict EID validation + existence check
; ------------------------------------------------------------------
SET isEidShaped=$$IsEID^utils(val) ; performs structural validation
IF isEidShaped DO QUIT eid ; if structure is not valid QUIT immediately
. IF $$IsEntityRegistered^apiVld(val) DO ; if raw EID exists (val)
. . SET eid=val
. . DO LOGDEBUG^logger("apiGet","GetEID: resolved via EID: "_val)
. ELSE DO LOGDEBUG^logger("apiGet","GetEID: "_val_" is a valid EID but is not registered")
; ------------------------------------------------------------------
; Path 1 — sys.attr.key lookup (aid omitted)
; Skipped when aid is supplied, also unreachable when val is EID-shaped
; ------------------------------------------------------------------
IF aid="" DO QUIT eid
. SET eid=$$ResolveLiveEID^apiRslv(AKEYID,val)
. IF eid=0 DO LOGDEBUG^logger("apiGet","GetEID: entity cannot be resolved via sys.attr.key: "_val)
. ELSE DO LOGDEBUG^logger("apiGet","GetEID: resolved via sys.attr.key: "_val)
; ---------------------------------------------------------------------------------
; Path 2 — unique attribute lookup (aid must be supplied)
; Skipped when aid is omitted, also unreachable when val is EID-shaped
; ----------------------------------------------------------------------------------
SET eid=$$ResolveLiveEID^apiRslv(aid,val)
IF eid=0 DO LOGDEBUG^logger("apiGet","GetEID: entity cannot be resolved via aid="_aid_" val="_val)
ELSE DO LOGDEBUG^logger("apiGet","GetEID: resolved via aid="_aid_" val="_val)
QUIT eid
L5 IsEID^utils
IsEID(val) ; check if entity ID is structurally valid
;;------------------------------------------------------------------
;; Function : $$IsEID^utils
;;
;; Purpose : Check if entity ID is structurally valid.
;;
;; Structural rules derived from GENID:
;; - Total length = 13 (hex timestamp) + 7 (base36 suffix) = exactly 20 characters (default RLEN=7)
;; - First 13 characters are lowercase hex: 0-9, a-f only
;; (timestamp produced by EPOCH via $ZCONVERT hex output)
;; - Last 7 characters are lowercase base36: 0-9, a-z only
;; - No uppercase characters permitted anywhere
;;
;; Parameters
;; val : (IN) eid string to test
;;
;; Returns :
;; 1 val is a structurally valid EID — exactly 20 chars,
;; first 13 lowercase hex, last 7 lowercase base36
;; 0 val fails structural validation
;;------------------------------------------------------------------
NEW len ; length of val
NEW ts ; first 13 chars — candidate hex timestamp
NEW sfx ; last 7 chars — candidate base36 suffix
NEW hexchars ; allowed lowercase hex character set
NEW b36chars ; allowed lowercase base36 character set
; ------------------------------------------------------------------
; Length must be EXACTLY 20 — not "at least 8" as before.
; ------------------------------------------------------------------
SET len=$LENGTH(val)
IF len'=20 QUIT 0
;. DO LOGDEBUG^logger("utils","IsEID: malformed EID — expected 20 chars, got "_len_" — "_val)
SET ts=$EXTRACT(val,1,13)
SET sfx=$EXTRACT(val,14,20)
; ------------------------------------------------------------------
; Prefix — 13 chars, lowercase hex only (0-9,a-f)
; ------------------------------------------------------------------
SET hexchars="0123456789abcdef"
IF $TRANSLATE(ts,hexchars)'="" DO QUIT 0
. DO LOGDEBUG^logger("utils","IsEID: malformed EID — non-hex timestamp segment — "_val)
; ------------------------------------------------------------------
; Suffix — 7 chars, lowercase base36 only (0-9,a-z)
; ------------------------------------------------------------------
SET b36chars="0123456789abcdefghijklmnopqrstuvwxyz"
IF $TRANSLATE(sfx,b36chars)'="" DO QUIT 0
. DO LOGDEBUG^logger("utils","IsEID: malformed EID — non-base36 suffix segment — "_val)
DO LOGDEBUG^logger("utils","IsEID: structurally valid — "_val)
QUIT 1
L5 IsEntityRegistered^apiVld
IsEntityRegistered(eid) ; check whether entity is registered in ^ABE
;;----------------------------------------------------------------------------------------------------
;; Function : IsEntityRegistered^apiVld
;;
;; Call : WRITE $$IsEntityRegistered^apiVld(eid)
;;
;; Purpose : Determine whether an ABox entity is registered in ^ABE.
;;
;; Parameters
;; eid : (IN) Entity identifier.
;;
;; Scope
;; Reads : ^ABE(eid,1)
;;
;; Returns : 1 if entity is registered in ^ABE, otherwise 0.
;;
;; Notes : registration is indicated by the existence of the node, not by a non-empty value e.g.
;; ^ABE("65825611ba143blkjysi",1)=""
;; ^ABE("65825611ba2aemmuqtyi",1)=""
;;
;; ''$DATA(...) double-negation canonicalize $DATA's four-valued return into a strict 0/1 boolean.
;;----------------------------------------------------------------------------------------------------
QUIT ''$DATA(^ABE(eid,1))
L5 ResolveLiveEID^apiRslv
ResolveLiveEID(aid,val) ; find the live eid whose current value for aid equals val, via ^TBDR/^AVET
;;------------------------------------------------------------------
;; Function : $$ResolveLiveEID^apiGet
;;
;; Purpose : Resolve val to its valkey under aid via ^TBDR, then walk ^AVET
;; candidates for that (aid,valkey) and return the one still live.
;;
;; Parameters
;; aid : (IN) Attribute ID whose value dictionary/index to search.
;; val : (IN) Literal value to resolve (natural key or unique attr value).
;;
;; Returns :
;; eid — the live candidate entity ID
;; 0 — no valkey registered for aid/val, or no live candidate found
;;------------------------------------------------------------------
NEW valkey ; resolved dictionary key for val under aid
NEW candidate ; current eid candidate walked from ^AVET
NEW eid ; live eid found, or 0
SET eid=0
SET valkey=$GET(^TBDR(aid,val))
IF valkey'="" DO
. SET candidate=""
. ; Iterate all eid candidates under (aid,valkey) in order — stop
. ; at the first one whose claim is still live. Presence in ^AVET
. ; only means "held this value at some point," not "holds it now."
. FOR SET candidate=$ORDER(^AVET(aid,valkey,candidate)) QUIT:(candidate="")!eid DO
. . IF $$GetCurrentVK^apiGet(candidate,aid)=valkey SET eid=candidate
QUIT eid
L3 ResolveByNaturalKey^apiRslv
ResolveByNaturalKey(ResolvedRec) ; resolve target entity via sys.attr.key. It finds or it creates EID
;;------------------------------------------------------------------
;; Function : ResolveByNaturalKey^apiRslv
;;
;; Purpose : ResolveTargeEntity/Path 1 — natural key resolution.
;; Resolves the target entity from sys.attr.key using
;; find-or-create semantics.
;;
;; If the key is already registered → return its existing eid.
;; If the key is not found → call GENID, create new entity that
;; is anchored to this sys.attr.key value on all future transactions.
;;
;; Parameters
;; ResolvedRec : (IN) Resolved record
;;
;; Returns :
;; eid — resolved or generated entity ID (find-or-create never fails)
;;------------------------------------------------------------------
NEW keyval ; sys.attr.key value read from ResolvedRec
NEW eid ; resolved or generated entity ID
SET keyval=$GET(ResolvedRec("+",AKEYID)) ; it must reach DatomAssert as entity fact
SET eid=$$GetEID^apiGet(keyval)
IF eid>0 DO
. DO LOGDEBUG^logger("apiRslv","ResolveByNaturalKey: resolved via sys.attr.key — "_keyval)
ELSE DO
. SET eid=$$GENID^utils
. DO LOGDEBUG^logger("apiRslv","ResolveByNaturalKey: new entity — generated EID "_eid)
QUIT eid
L4 GENID^utils
GENID(RLEN) ; Generate a Time-Sorted Compact ID (KSUID-style)
;;------------------------------------------------------------------
;; Routine : GENID Function
;;
;; Call : WRITE $$GENID^<routine>(RLEN)
;;
;; Usage :
;; SET ID=$$GENID^<routine>()
;; SET ID=$$GENID^<routine>(10)
;;
;; Purpose :
;; Generates a compact, chronologically sortable unique identifier
;; similar in concept to a KSUID.
;;
;; The identifier consists of:
;; - A 13-character hexadecimal timestamp prefix generated
;; by $$EPOCH
;; - A random Base36 suffix of configurable length
;;
;; This structure preserves time ordering while reducing the
;; likelihood of collisions between generated IDs.
;;
;; Scope
;; Reads : $$EPOCH, $RANDOM
;; Writes : CHARSET, RAND, I, POS, RLEN (local variables)
;;
;; Parameters
;; RLEN : (IN)
;; Length of the random Base36 suffix.
;; Defaults to 7 if not supplied.
;;
;; Returns :
;; A lowercase time-sorted identifier in the format:
;;
;; [Hex Timestamp(13)][Random Base36 Suffix(RLEN)]
;;
;; Example:
;; 66a1f4c9d2e7b4k9x2qa
;;
;; Notice :
;; - Uses Base36 characters: 0-9 and a-z.
;; - Timestamp ordering allows IDs to sort chronologically.
;; - Collision probability depends on suffix length and
;; generation frequency within the same microsecond.
;; - Requires the EPOCH function to be available in the
;; same routine or namespace.
;;
;;------------------------------------------------------------------
NEW CHARSET,RAND,I,POS
; Ensure RLEN has a value; default to 7 if not provided
SET RLEN=$G(RLEN,7)
; Define the character set for the random suffix (Base36: 0-9, a-z)
SET CHARSET="0123456789abcdefghijklmnopqrstuvwxyz"
SET RAND=""
; Generate the random portion of the ID
; This provides the entropy needed to prevent collisions in the same ms
FOR I=1:1:RLEN DO
. SET POS=$RANDOM($L(CHARSET))+1
. SET RAND=RAND_$E(CHARSET,POS)
; Concatenate and return the result in lowercase
; The result is [Timestamp(13)][Random(RLEN)]
QUIT $$EPOCH_RAND
L3 ResolveByUnique^apiRslv
ResolveByUnique(ResolvedRec) ; resolves target entity via a unique attribute optionally using sys.attr.resolvedby. It finds or it creates EID
;;--------------------------------------------------------------------------------------------------------------
;; Function : ResolveByUnique^apiRslv
;;
;; Purpose : ResolveTargeEntity/Path 2 — resolve target entity via a unique attribute optionally using sys.attr.resolvedby.
;; It finds or it creates EID. Dispatches to the correct resolution sub-path based
;; on the count of unique attributes present in ResolvedRec and the presence of sys.attr.resolvedby.
;;
;; Parameters
;; ResolvedRec : (IN/OUT) Resolved record — sys.attr.resolvedby node if present is killed
;;
;; Returns :
;; eid — resolved or generated entity ID
;; 0 — any resolution or validation failure
;;----------------------------------------------------------------------------------------------------------------
NEW count ; number of unique attributes present in ResolvedRec
NEW firstAid ; aid of the sole unique attribute, when count=1
NEW eid ; resolved or generated entity ID
SET eid=0 ; initialize eid, assume resolve failed
DO CountUniqueAttributes(.ResolvedRec,.count,.firstAid)
; resolvedby attribute present but no unique attributes — error
IF count=0,$DATA(ResolvedRec("+",RESOLVEDBYID)) DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveByUnique: sys.attr.resolvedby specified but no unique attributes were supplied in the transaction record.")
; No unique attributes and no resolvedby attribute — anonymous entity
IF count=0 DO QUIT eid
. SET eid=$$GENID^utils
. DO LOGWARNING^logger("apiRslv","ResolveByUnique: generated EID "_eid)
; Exactly one unique attribute — pass firstAid directly
IF count=1 QUIT $$ResolveOwnership(firstAid,.ResolvedRec)
; More than one unique attribute — resolvedby required
IF '$DATA(ResolvedRec("+",RESOLVEDBYID)) DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveByUnique: multiple unique attributes require sys.attr.resolvedby")
SET eid=$$ResolveDesignatedUnique(.ResolvedRec)
; remove sys.attr.resolvedby ResolvedRec node — it must not reach DatomAssert as entity fact
IF $DATA(ResolvedRec("+",RESOLVEDBYID)) DO
. KILL ResolvedRec("+",RESOLVEDBYID)
QUIT eid
L4 CountUniqueAttributes^apiRslv
CountUniqueAttributes(ResolvedRec,count,firstAid)
;;------------------------------------------------------------------
;; Procedure : CountUniqueAttributes^apiRslv
;; Call : DO CountUniqueAttributes^apiRslv(.ResolvedRec,.count,.firstAid)
;;
;; Purpose : Scans ResolvedRec in a single pass and counts how many
;; assertion attributes carry a uniqueness constraint
;; (UNQINSERT or UNQUPSERT). Returns the count and the
;; first unique aid found.
;;
;; This routine only counts — it does not resolve, lookup,
;; stage, or validate. It exists solely to inform
;; ResolveByUnique() which resolution path to take.
;;
;; Parameters
;; ResolvedRec : (IN) Resolved record — assertions only
;; count : (OUT) Number of unique attributes found
;; firstAid : (OUT) Aid of first unique attribute found
;; "" if count=0
;;
;; Returns : Nothing (procedure)
;;------------------------------------------------------------------
NEW aid
SET count=0
SET firstAid=""
SET aid=""
FOR SET aid=$ORDER(ResolvedRec("+",aid)) QUIT:aid="" DO
. IF $$GetUniqueMode^sapiGet(aid) DO
. . SET count=count+1
. . IF firstAid="" SET firstAid=aid
QUIT
L5 GetUniqueMode^sapiGet
GetUniqueMode(aid)
;;------------------------------------------------------------------
;; Function : GetUniqueMode^sapiGet
;; Call : SET mode=$$GetUniqueMode^sapiGet(aid)
;;
;; Purpose : Returns the uniqueness constraint mode for a given
;; attribute as a schema enumeration EID.
;;
;; Replaces the pair $$IsUniqueInsert/$$IsUniqueUpsert
;; with a single function that returns the mode directly,
;; allowing callers to compare against schema EIDs rather
;; than invented string constants.
;;
;; Parameters
;; aid : (IN) Attribute entity ID
;;
;; Returns :
;; UNQINSERTID — attribute enforces insert-unique (:db.unique/value)
;; UNQUPSERTID — attribute enforces upsert-unique (:db.unique/identity)
;; 0 — attribute has no uniqueness constraint
;;------------------------------------------------------------------
IF ''$DATA(^TBAVET(UNIQUEID,INSERTKEY,aid)) QUIT UNQINSERTID
IF ''$DATA(^TBAVET(UNIQUEID,UPSERTKEY,aid)) QUIT UNQUPSERTID
QUIT 0
L4 ResolveDesignatedUnique^apiRslv
ResolveDesignatedUnique(ResolvedRec) ; resolve target entity via caller-specified unique attribute (sys.attr.resolvedby)
;;------------------------------------------------------------------
;; Function : $$ResolveDesignatedUnique^apiRslv
;;
;; Purpose : Resolves the target entity when multiple unique
;; attributes are present and the caller has supplied
;; sys.attr.resolvedby to identify which one to use.
;;
;; Performs four validations before delegating to
;; ResolveOwnership:
;;
;; 1. Attribute key exists in schema
;; 2. Attribute is unique (UNQINSERT or UNQUPSERT)
;; 3. Attribute is present in ResolvedRec
;; 4. Attribute value is non-empty
;;
;; Only if all four pass does it call ResolveOwnership.
;;
;; Parameters
;; ResolvedRec : (IN) Resolved record — sys.attr.resolvedby already
;; present; read here, killed by the caller
;; (ResolveByUnique) once this returns
;;
;; Returns :
;; eid — successfully resolved or generated
;; 0 — any validation failure or UNQINSERT violation
;;------------------------------------------------------------------
NEW attrkey ; sys.attr.resolvedby value — schema key naming the attribute to resolve by
NEW aid ; resolved attribute ID for attrkey
NEW val ; value of that attribute in ResolvedRec
NEW eid ; resolved or generated entity ID
SET eid=0
SET attrkey=$GET(ResolvedRec("+",RESOLVEDBYID))
; Validation 1 — attribute key must exist in schema
SET aid=$$GetEID^sapiGet(attrkey)
IF aid<1 DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveDesignatedUnique: resolvedby attribute is not registered in the schema — "_attrkey)
; Validation 2 — attribute must be unique
IF '$$GetUniqueMode^sapiGet(aid) DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveDesignatedUnique: resolvedby attribute is not unique — "_attrkey)
; Validation 3 — attribute must be present in record
IF '$DATA(ResolvedRec("+",aid)) DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveDesignatedUnique: resolvedby attribute is missing from record — "_attrkey)
; Validation 4 — value must be non-empty
SET val=$GET(ResolvedRec("+",aid))
IF val="" DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveDesignatedUnique: resolvedby attribute has an empty value — "_attrkey)
QUIT $$ResolveOwnership(aid,.ResolvedRec)
L5 ResolveOwnership^apiRslv
ResolveOwnership(aid,ResolvedRec) ; resolve target entity for one unique attribute via UNQINSERT/UNQUPSERT semantics
;;------------------------------------------------------------------------------------------------------------------
;; Function : $$ResolveOwnership^apiRslv
;;
;; Purpose : Resolves the target entity EID for a single unique
;; attribute by applying UNQINSERT or UNQUPSERT semantics.
;;
;; Parameters
;; aid : (IN) Attribute entity ID (must be unique)
;; ResolvedRec : (IN) Resolved record
;;
;; Returns :
;; eid — successfully resolved or generated
;; 0 — empty value (internal error) or UNQINSERT violation
;;------------------------------------------------------------------
NEW val ; value supplied for this unique attribute
NEW mode ; UNQINSERT or UNQUPSERT
NEW ownereid ; eid currently holding valkey, if any (may be stale)
NEW ownerIsLive ; whether ownereid's claim on valkey is still active
;
; ownereid/ownerIsLive: ^AVET keeps a subscript for every eid that ever claimed valkey,
; even a dead one — e.g. F23: Carol claims personal@example.com, retracts it, then Erin
; claims the same value. Both Carol's and Erin's eids now sit under the same (aid,valkey),
; in no guaranteed order, but only Erin's claim is live. Taking whichever eid $ORDER finds
; first would risk picking Carol — ResolveLiveEID walks all candidates and returns the one
; whose claim is still live (0 if none is), so ownereid is always the true current owner,
; not just the first subscript. This distinction changes the outcome by mode:
; - UNQINSERT: no live owner means the value is free again — allow a new entity instead
; of rejecting forever.
; - UNQUPSERT: no live owner means the prior identity released this value — attach a new
; eid instead of silently merging into a dead identity
;
NEW eid ; resolved or generated entity ID
SET eid=0
SET val=$GET(ResolvedRec("+",aid))
SET mode=$$GetUniqueMode^sapiGet(aid)
IF val="" DO QUIT 0
. DO LOGERROR^logger("apiRslv","ResolveOwnership: internal error — empty value for unique attribute ("_aid_")")
IF $$IsRangeValue^sapiVld(aid,val) DO
. SET ownereid=$$ResolveLiveEID(aid,val)
. SET ownerIsLive=(ownereid'=0)
.
. ; UNQINSERT — strict insert, never merges. Violation only while owner is still live.
. IF mode=UNQINSERTID,ownerIsLive DO QUIT
. . SET eid=0
. . DO LOGERROR^logger("apiRslv","ResolveOwnership: insert-unique violation — ("_aid_","_val_") already owned by ("_ownereid_")")
.
. ; UNQINSERT, no live owner — value is free again, treat as a brand-new entity.
. IF mode=UNQINSERTID,'ownerIsLive DO QUIT
. . SET eid=$$GENID^utils
. . DO LOGDEBUG^logger("apiRslv","ResolveOwnership: insert-unique — value released or new, generated EID "_eid)
.
. ; UNQUPSERT, owner still live — this claimant IS that same real-world identity.
. IF mode=UNQUPSERTID,ownerIsLive DO QUIT
. . SET eid=ownereid
. . DO LOGDEBUG^logger("apiRslv","ResolveOwnership: upsert-unique merge → existing EID ("_eid_")")
.
. ; UNQUPSERT, no live owner — a new claimant here is a DIFFERENT identity, so mint
. ; a fresh eid instead of merging into the dead owner.
. IF mode=UNQUPSERTID,'ownerIsLive DO QUIT
. . SET eid=$$GENID^utils
. . DO LOGDEBUG^logger("apiRslv","ResolveOwnership: upsert-unique — prior identity released, generated new EID "_eid)
; val has never been registered at all — new entity, regardless of mode.
ELSE DO
. SET eid=$$GENID^utils
. DO LOGDEBUG^logger("apiRslv","ResolveOwnership: new entity — generated EID "_eid)
QUIT eid
L2 IsEntityRegistered^apiVld
IsEntityRegistered(eid) ; check whether entity is registered in ^ABE
;;-------------------------------------------------------------------------
;; Function : IsEntityRegistered^apiVld
;;
;; Call : WRITE $$IsEntityRegistered^apiVld(eid)
;;
;; Purpose : Determine whether an ABox entity is registered in ^ABE.
;;
;; Parameters
;; eid : (IN) Entity identifier.
;;
;; Scope
;; Reads : ^ABE(eid,1)
;;
;; Returns : 1 if entity is registered in ^ABE, otherwise 0.
;;
;; Notes : registration is indicated by the existence of the node, not by a non-empty value e.g.
;; ^ABE("65825611ba143blkjysi",1)=""
;; ^ABE("65825611ba2aemmuqtyi",1)=""
;;
;; ''$DATA(...) double-negation canonicalize $DATA's four-valued return into a strict 0/1 boolean.
;;---------------------------------------------------------------------------
QUIT ''$DATA(^ABE(eid,1))
L2 ValidateRecord^apiVld
ValidateRecord(ResolvedRec,ReqAttr,AttrCache,isNewEntity)
;;------------------------------------------------------------------
;; Function : ValidateRecord^apiVld
;; Call : SET ok=$$ValidateRecord^apiVld(.ResolvedRec,.ReqAttr,.AttrCache,isNewEntity)
;;
;; Purpose : Validates entity facts in ResolvedRec after control
;; attributes have been consumed by ResolveTargetEntity.
;; At this point ResolvedRec contains ONLY entity facts —
;; no sys.attr.id, sys.attr.key, or sys.attr.resolveby.
;;
;; Two checks are performed:
;;
;; CHECK 1 — Empty values
;; Every assertion must have a non-empty value.
;; Null subscripts crash ^TBDR and ^EATV globals on YottaDB.
;; Applied unconditionally — insert or update.
;;
;; CHECK 2 — Required attributes
;; Every attribute marked as required in the schema must
;; be present in ResolvedRec. Applied ONLY when
;; isNewEntity is true — i.e. only when creating a brand
;; new entity. Updates to an existing entity may
;; legitimately assert only a subset of its attributes;
;; the required-attribute set was already satisfied when
;; the entity was first created, and re-demanding every
;; required attribute on every subsequent update would
;; make partial updates impossible.
;;
;; When applied, this check is further scoped to records
;; that contain at least one attribute from the same
;; sub-namespace as the required attributes — prevents
;; cross-namespace false failures e.g. a person.* record
;; must not be checked against sandbox.movie.* required
;; attributes. Namespace membership is resolved via
;; AttrCache(aid) reverse map — O(1) lookup, no schema
;; traversal per attribute.
;;
;; Both checks concern the entity itself — not the
;; transaction. This is Phase 2 validation, distinct from
;; the transaction validation performed inside
;; ResolveTargetEntity (Phase 1).
;;
;; Parameters
;; ResolvedRec : (IN) Resolved record — entity facts only
;; ReqAttr : (IN) Required attributes map — built by
;; BuildAttrsRequired^sapi
;; ReqAttr = total count of required attrs
;; ReqAttr(aid) = attrkey string
;; AttrCache : (IN) Schema attribute cache — built by
;; BuildAttrsCache^sapi
;; AttrCache(fullkey) = aid (forward map)
;; AttrCache(aid) = sub-namespace (reverse map)
;; isNewEntity : (IN) 1 if this record is creating a brand new
;; entity (eid did not previously exist in
;; ^EATV), 0 if updating an existing entity.
;; CHECK 2 is skipped entirely when 0.
;; Defaults to 1 if omitted — preserves strict
;; create-time validation for direct callers
;; (e.g. standalone tests) that don't supply it.
;;
;; Returns :
;; 1 — all checks passed
;; 0 — any check failed (error logged)
;;------------------------------------------------------------------
NEW aid,ok,missingCount,reqns,hasNamespaceAttr
SET ok=1
SET isNewEntity=$GET(isNewEntity,1)
; ------------------------------------------------------------------
; CHECK 1 — Empty values
; Null subscripts crash ^TBDR and ^EATV globals on YottaDB.
; Check assertions only — retractions allow empty val for
; cardinality-one (DatomRetract handles that case itself).
; Applied unconditionally regardless of isNewEntity.
; ------------------------------------------------------------------
; Iterate all assertions — fail fast on first empty value
SET aid=""
FOR SET aid=$ORDER(ResolvedRec("+",aid)) QUIT:aid=""!'ok DO
. IF ResolvedRec("+",aid)="" SET ok=0 DO LOGERROR^logger("apiVld","ValidateRecord: empty value for aid "_aid_" — aborting")
IF 'ok QUIT 0
; ------------------------------------------------------------
; CHECK 2 — Attribute predicates
; ------------------------------------------------------------
IF '$$ValidateAttrPredicates(.ResolvedRec) QUIT 0
; ------------------------------------------------------------------
; CHECK 3 — Required attributes
; Skipped entirely for updates to an existing entity.
; ------------------------------------------------------------------
; Short-circuit — no required attrs defined for this namespace
IF ReqAttr=0 QUIT ok
; Short-circuit — updating an existing entity, not creating one
IF 'isNewEntity QUIT ok
; Determine the required sub-namespace from the first ReqAttr entry
SET aid=$ORDER(ReqAttr(""))
SET reqns=$PIECE($GET(ReqAttr(aid)),".",1,2)
; Check whether the record contains any attribute from the required namespace
; Uses AttrCache reverse map — AttrCache(aid) = sub-namespace
SET hasNamespaceAttr=0
SET aid=""
FOR SET aid=$ORDER(ResolvedRec("+",aid)) QUIT:aid=""!hasNamespaceAttr DO
. IF $GET(AttrCache(aid))=reqns SET hasNamespaceAttr=1
; Skip required attribute check — record belongs to a different namespace
IF 'hasNamespaceAttr QUIT ok
; Iterate required attributes and verify each is present in the record
SET missingCount=0
SET aid=""
FOR SET aid=$ORDER(ReqAttr(aid)) QUIT:aid="" DO
. IF '$DATA(ResolvedRec("+",aid)) DO
. . SET missingCount=missingCount+1
. . DO LOGERROR^logger("apiVld","ValidateRecord: Record is missing required attribute "_$GET(ReqAttr(aid),aid))
IF missingCount>0 DO
. SET ok=0
. DO LOGERROR^logger("apiVld","ValidateRecord: Record is missing "_missingCount_" required attribute(s)")
QUIT ok
L3 ValidateAttrPredicates^apiVld
ValidateAttrPredicates(ResolvedRec)
; ------------------------------------------------------------
; Validate schema-defined attribute predicates.
; Returns:
; 1 = all predicates passed
; 0 = one or more predicates failed
; ------------------------------------------------------------
NEW aid,val,predicate,ok
SET ok=1
SET aid=""
; Iterate every asserted attribute (aid) on this record. For
; each one, look up whether the schema defines a validation
; predicate for that attribute; if it does, run the predicate
; against the attribute's staged value. Stops at the first
; failing predicate — remaining attributes are not checked
FOR SET aid=$ORDER(ResolvedRec("+",aid)) QUIT:aid=""!'ok DO
. NEW predicate,val
. ; Retrieve the schema predicate for this attribute.
. SET predicate=$$GetAttrValidFn^sapiGet(aid)
. ; No predicate defined for this attribute — nothing to enforce.
. IF predicate="" QUIT
. SET val=ResolvedRec("+",aid)
. IF '$$ValidateAttrPredVal(predicate,val) DO
. . SET ok=0
. . DO LOGERROR^logger("apiVld","ValidateAttrPredicates: validation failed for aid="_aid_", value="""_val_""", predicate="_predicate)
QUIT ok
L4 GetAttrValidFn^sapiGet
GetAttrValidFn(aid)
;;------------------------------------------------------------------
;; Function : GetAttrValidFn^sapiGet
;; Call : SET fn=$$GetAttrValidFn^sapiGet(aid)
;;
;; Purpose : Returns the validation predicate associated with an
;; attribute, or "" if none has been defined.
;;
;; ^TBEAVT structure: ^TBEAVT(entity,attribute,valuekey,txn)=1
;; entity=aid, attribute=VALIDFNID — direct EAVT lookup, no scan.
;;
;; Returns :
;; predicate string (e.g. "$$IsInt^utils")
;; "" if no validation predicate exists
;;------------------------------------------------------------------
NEW vk
IF '$DATA(VALIDFNID) QUIT ""
IF VALIDFNID'>0 QUIT ""
SET vk=$ORDER(^TBEAVT(aid,VALIDFNID,""))
IF vk="" QUIT ""
QUIT $$GetDictValue(VALIDFNID,vk)
L4 ValidateAttrPredVal^apiVld
ValidateAttrPredVal(predicate,val)
; ------------------------------------------------------------
; Procedure : ValidateAttrPredVal^apiVld
;
; Purpose : Executes schema-defined validation predicate
; against a single attribute value.
;
; predicate : fully-qualified predicate, e.g. "$$IsInt^utils"
; (leading "$$" optional — stripped if present)
; val : the attribute value to validate
;
; NOTE: Indirect call syntax ($$@fn(val), $$@call) does not work
;
; Returns : 1 if predicate returned true, 0 otherwise
; ------------------------------------------------------------
NEW result,safeVal
SET result=0
IF predicate="" QUIT 1
; Escape embedded quotes so val cannot terminate the string
; literal early and inject code into the executed command.
SET safeVal=$$EscapeQuotes(val)
XECUTE "SET result="_predicate_"("""_safeVal_""")"
QUIT result
L2 DatomAssert^apiWFL
DatomAssert(eid,aid,val) ; dispatch to the single- or multi-value staging branch by attribute cardinality
;;----------------------------------------------------------------------------------------------
;; Function : DatomAssert^apiWFL
;;
;; Purpose : Dispatches to DatomAssertSingle or DatomAssertMulti based on the attribute's
;; cardinality. Assumes %ABR has already been initialised by Stage.
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID of the assertion predicate
;; val : (IN) Literal or reference value (can be multi-value "val1|val2" when cardinality-many)
;;
;; Returns
;; 0 : triplet assertion failed
;; 1 : triplet staged successfully
;; 2 : no-op — value already current, nothing staged (cardinality-one path only)
;;----------------------------------------------------------------------------------------------
IF $$IsMultiValue^sapiVld(aid) QUIT $$DatomAssertMulti(eid,aid,val)
QUIT $$DatomAssertSingle(eid,aid,val)
L3 IsMultiValue^sapiVld
IsMultiValue(aid)
;;------------------------------------------------------------------
;; Function : IsMultiValue^sapiVld
;; Call : IF $$IsMultiValue^sapiVld(aid)
;;
;; Purpose : To support delimited multi-value expansion using "|" for attributes flagged as multi-valued. (see ^api routines)
;;
;;
;; Returns :
;; 1 — multi-valued attribute (cardinality many)
;; 0 — single-valued attribute (any other cardinality)
;;
;; Notes :
;; : ''$DATA(...) double-negation canonicalize $DATA's four-valued return into a strict 0/1 boolean.
;;------------------------------------------------------------------
NEW isMulti
SET isMulti=''$DATA(^TBAVET(CARDINID,MANYKEY,aid))
;IF 'isMulti DO LOGDEBUG^logger("sapiVld","IsMultiValue: not multi-valued — aid="_aid)
QUIT isMulti
L3 DatomAssertMulti^apiWFL
DatomAssertMulti(eid,aid,val) ; recurse per "|" token, then stage each via DatomStageMulti
;;----------------------------------------------------------------------------------------------
;; Function : DatomAssertMulti^apiWFL
;;
;; Purpose : Splits "|"-delimited val into independent DatomAssertMulti calls, or — for a
;; single token — delegates staging to DatomStageMulti with op=1.
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID of the assertion predicate — must be cardinality-many
;; val : (IN) Literal or reference value (can be multi-value "val1|val2")
;;
;; Returns
;; 1 : triplet(s) staged successfully
;; 2 : no-op — single token only, value already active, nothing staged
;; 0 : one or more triplets failed resolution/staging
;;----------------------------------------------------------------------------------------------
NEW isAllOk ; overall success flag returned to caller — cleared if any recursive/staging step fails
NEW pIdx ; 1-based piece index while splitting val on "|"
NEW pieceVal ; current "|"-delimited token during multi-value recursion
NEW result ; DatomStageMulti's return for this single token
SET isAllOk=1
; Delimiter present — recurse per "|"-delimited token as an independent datom insertion.
IF val["|" DO QUIT isAllOk
. FOR pIdx=1:1 SET pieceVal=$PIECE(val,"|",pIdx) QUIT:pieceVal=""!'isAllOk DO
. . IF '$$DatomAssertMulti(eid,aid,pieceVal) SET isAllOk=0
SET result=$$DatomStageMulti(eid,aid,val,1)
IF result=0 QUIT 0
IF (result=1)&(aid=ALIASID) DO AddKeyword(eid,val) ; add alias as a keyword so it can be used in ydb console
QUIT result
L4 DatomStageMulti^apiWFL
DatomStageMulti(eid,aid,val,op) ; stage a cardinality-many assert or retract for (eid,aid,val)
;;----------------------------------------------------------------------------------------------
;; Function : DatomStageMulti^apiWFL
;;
;; Purpose : Resolves val to a valkey. If newly allocated, it has no history in ^EATV, so
;; assert creates it directly and retract fails directly — no scan needed. If the
;; valkey already existed, walks ^EATV(eid,aid,*) from the most recent transaction
;; backward until this valkey is found, and stages the requested polarity into %ABR
;; based on its recorded state relative to the requested op.
;;
;; Algorithm:
;; 1. Resolve val → valkey.
;; 2. NEW VALKEY (isNewValKey=1 — never existed before this call, no ^EATV history
;; is possible)
;; assert → stage op=1 CREATE
;; retract → fail, nothing to retract FAIL
;; 3. EXISTING VALKEY (isNewValKey=0) — scan transactions for (eid,aid)
;; newest-to-oldest; stop at the first one where valkey is recorded — that op
;; IS the valkey's current state, since any more recent write would already
;; have been seen.
;; 4. NOT FOUND (valkey has no recorded op in ^EATV despite being a known key)
;; assert → stage op=1 CREATE
;; retract → fail, nothing to retract FAIL
;; 5. FOUND, recorded op equals requested op
;; → no-op, nothing staged NO-OP
;; 6. FOUND, recorded op differs from requested op
;; → stage requested op FLIP
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID — must be cardinality-many
;; val : (IN) Single value to assert/retract — must not contain "|"
;; op : (IN) 1 to assert, 0 to retract
;;
;; Returns
;; 1 : datom staged (CREATE or FLIP path)
;; 2 : no-op — recorded state already matches requested op
;; 0 : failed — value resolution failed, or valkey never recorded when retracting
;;
;; Scope
;; Reads : ^EATV
;;
;; Notes
;; Branch 4 (existing valkey, not found in ^EATV) is kept defensively — pending confirmation
;; of whether valkey registration can ever occur without a prior ^EATV write for it. If not,
;; isNewValKey=0 always implies found=1 and branch 4 is unreachable.
;;----------------------------------------------------------------------------------------------
NEW valkey ; resolved value-type key for val
NEW isNewValKey ; resolution result — 1 new valkey allocated, 0 existing valkey reused, <0 resolution failure
NEW curtx ; tx cursor, walking ^EATV(eid,aid,*) from most recent transaction backward
NEW found ; 1 once valkey has been located at some transaction — stops the scan
NEW recop ; op recorded at the transaction where valkey was found; meaningful only if found=1
SET valkey=""
; Resolve val into valkey. Reference-typed attributes resolve/register via ResolveAttrRef,
; everything else registers as a literal via RegisterAttrVal.
IF $$IsRefValType^sapiVld(aid) DO
. SET isNewValKey=$$ResolveAttrRef^sapiRslv(aid,val,.valkey,"ABox")
ELSE DO
. SET isNewValKey=$$RegisterAttrVal^sapiRslv(aid,val,.valkey)
; Resolution failure (-1) must abort immediately — the resolver has already logged the cause.
IF isNewValKey<0 DO QUIT 0
. DO LOGERROR^logger("apiWFL","DatomStageMulti: aborting — value resolution failed for (eid="_eid_",aid="_aid_",val="_val_")")
; New valkey, retract — never existed, nothing to retract, fail. No scan needed.
IF isNewValKey&('op) DO QUIT 0
. DO LOGDEBUG^logger("apiWFL","DatomStageMulti: failed — ("_eid_","_aid_","_valkey_") is a new valkey, nothing to retract")
; New valkey, assert — create directly. No scan needed.
IF isNewValKey&(op=1) DO QUIT 1
. SET %ABR(eid,aid,valkey)=1
. DO LOGDEBUG^logger("apiWFL","DatomStageMulti: create staged ("_eid_","_aid_","_valkey_") — new valkey")
; Existing valkey — walk transactions for (eid,aid) newest-to-oldest. The first transaction
; where this valkey is recorded IS its current state — any more recent write would already
; have been visited first. Stop as soon as it's found.
SET found=0
SET recop=0
SET curtx=$ORDER(^EATV(eid,aid,""),-1)
FOR QUIT:(curtx="")!found DO
. IF $$IsDatomRecorded^apiVld(eid,aid,curtx,valkey) DO
. . SET recop=^EATV(eid,aid,curtx,valkey)
. . SET found=1
. ELSE SET curtx=$ORDER(^EATV(eid,aid,curtx),-1) ; step backwards to previous transaction
; Existing valkey, not found in ^EATV, retract — fail
IF 'found&('op) DO QUIT 0
. DO LOGDEBUG^logger("apiWFL","DatomStageMulti: failed — ("_eid_","_aid_","_valkey_") never recorded")
; Existing valkey, not found in ^EATV, assert — create
IF 'found&(op=1) DO QUIT 1
. SET %ABR(eid,aid,valkey)=1
. DO LOGDEBUG^logger("apiWFL","DatomStageMulti: create staged ("_eid_","_aid_","_valkey_")")
; Found, recorded state already matches requested op — no-op
IF recop=op DO QUIT 2
. DO LOGDEBUG^logger("apiWFL","DatomStageMulti: no-op — ("_eid_","_aid_","_valkey_") already op="_op)
; Found, recorded state differs from requested op — flip it
SET %ABR(eid,aid,valkey)=op
DO LOGDEBUG^logger("apiWFL","DatomStageMulti: staged ("_eid_","_aid_","_valkey_")="_op)
QUIT 1
L5 IsRefValType^sapiVld
IsRefValType(aid)
;;------------------------------------------------------------------
;; Function : IsRefValType^sapiVld
;; Call : IF $$IsRefValType^sapiVld(aid)
;;
;; Purpose : Returns 1 if the metadata attribute is reference-typed, 0 otherwise.
;; A reference data type stores entity IDs as values
;;
;;
;; Returns :
;; 1 — reference data type (values are entity IDs)
;; 0 — literal type (values are raw strings)
;;
;; Notes :
;; ''$DATA(...) double-negation canonicalize $DATA's four-valued return into a strict 0/1 boolean.
;;------------------------------------------------------------------
NEW isRef
SET isRef=''$DATA(^TBAVET(RANGEID,REFKEY,aid))
;IF 'isRef DO LOGDEBUG^logger("sapiVld","IsRefValType: not reference-typed — aid="_aid)
QUIT isRef
L5 ResolveAttrRef^sapiRslv
ResolveAttrRef(aid,refeid,valkey,modelType) ; resolve the value key for an ABox reference attribute value
;;----------------------------------------------------------------------------------------------------
;; Function : ResolveAttrRef^sapiRslv
;;
;; Call : SET isNewValKey=$$ResolveAttrRef^apiRslv(aid,refeid,.valkey)
;;
;; Usage : ABox counterpart to ResolveAttrRef^sapiRslv. Called by
;; DatomAssert^apiWFL and DatomRetract^apiWFL, ONLY for
;; attributes where IsRefValType^sapiVld(aid) is true.
;;
;; Purpose : Resolves the lookup key (valkey) for a reference-typed
;; attribute (aid). refeid may arrive as any of three forms,
;; each resolved by its own function below:
;; • isa attribute value (label or eid) -> ResolveIsaRef
;; • an ordinary key label -> ResolveKeyLabel
;; • an already-resolved EID -> ResolveRawEid
;;
;; Parameters
;; aid : (IN) Reference-typed attribute entity ID
;; refeid : (IN) ABox entity ID in one of the three forms above
;; modelType : (IN) Model layer containing the entity "ABox" or "TBox"
;; valkey : (OUT) Lookup value key for refeid within aid's data type
;;
;; Returns :
;; 1 — new value key allocated
;; 0 — existing value key reused
;; -1 — cannot be resolved; valkey is left ""
;;----------------------------------------------------------------------------------------------------
NEW result
SET valkey="" ; final value if resolution fails
; refeid is handled by exactly one of the two branches below:
; isa value -> ResolveIsaRef (label or raw TBox eid, registered directly)
; key label -> resolved to an eid via GetEID^apiGet
; raw EID -> validated directly (format, then existence)
; every branch either registers the value (success) or sets result=RFailed;
; valkey is only ever populated by RegisterAttrVal^sapiRslv, so it stays "" on every failure path.
IF aid=ISAID SET result=$$ResolveIsaRef(aid,refeid,.valkey)
ELSE IF $$IsRangeValue^sapiVld(AKEYID,refeid) SET result=$$ResolveKeyLabel(aid,refeid,.valkey,modelType)
ELSE SET result=$$ResolveRawEid(aid,refeid,.valkey,modelType)
QUIT result
== L6 ResolveIsaRef^sapiRslv
ResolveIsaRef(aid,refeid,valkey) ; resolve isa attribute values (key label or raw eid) against the TBox entity id
;;------------------------------------------------------------------
;; Function : ResolveIsaRef^sapiRslv
;;
;; Call : SET result=$$ResolveIsaRef^sapiRslv(aid,refeid,.valkey)
;;
;; Usage : Private helper for ResolveAttrRef^sapiRslv; not called elsewhere.
;;
;; Purpose : The isa attribute (aid=ISAID) always takes a TBox type as its value,
;; arriving as either a key label (e.g. "sys.type.obj") or an
;; already-resolved eid (e.g. 900). GetEID^sapiGet normalizes either
;; form to an eid; the eid is then registered directly against
;; ^TBDR(ISAID,*) rather than going through ResolveRawEid/ResolveKeyLabel,
;; since isa values are TBox dictionary literals, not ABox hash references.
;;
;; Parameters
;; aid : (IN) ISAID (299) — passed through for symmetry/logging only
;; refeid : (IN) TBox type as a key label or raw eid (e.g. "sys.type.obj", 1013)
;; valkey : (OUT) 1 new / 0 existing value key, or -1 on failure
;;
;; Notes : valkey (by reference) is populated by RegisterAttrVal^sapiRslv
;;------------------------------------------------------------------
NEW eid,result,msg
w "ResolveIsaRef: resolving isa value '"_refeid_"' for aid="_aid,!
SET eid=$$GetEID^sapiGet(refeid)
IF eid="" DO QUIT -1
. DO LOGERROR^logger("sapiRslv","ResolveIsaRef: could not resolve '"_refeid_"' to an eid — aid="_aid)
; Defensive: eid must actually be a registered TBox entity, otherwise a
; typo'd/unresolvable isa value would silently mint a bogus new dictionary
; entry instead of failing loudly.
IF '$$IsEntityRegistered^sapiVld(eid) DO QUIT -1
. DO LOGERROR^logger("sapiRslv","ResolveIsaRef: refeid '"_refeid_"' (eid="_eid_") is not a known TBox entity — aid="_aid)
SET msg="ResolveIsaRef: registering isa value eid="_eid_" (from refeid="_refeid_") for aid="_aid
DO LOGDEBUG^logger("sapiRslv",msg)
SET result=$$RegisterAttrVal^sapiRslv(aid,eid,.valkey)
QUIT result
== L6 IsRangeValue^sapiRslv
IsRangeValue(aid,val) ; check if val (literal or reference) is already registered in aid's data type
;;------------------------------------------------------------------
;; Function : IsRangeValue^sapiVld
;;
;; Call : IF $$IsRangeValue^sapiVld(aid,val)
;;
;; Usage : Called by IsNewRangeValue to test literal (non-reference) values before insertion
;;
;; Purpose : Returns 1 if the candidate value (val) is already registered in the data type of attribute (aid)
;;
;; Parameters
;; aid : (IN) Attribute entity ID identifying the data type being checked
;; val : (IN) Candidate value being checked for existing registration
;;
;; Scope
;; Reads : ^TBDR(aid,val)
;;
;; Returns :
;; 1 : val is already registered
;; 0 : val is not yet registered
;;
;; Notes : Used by IsNewRangeValue for literal data types only.
;; ResolveAV handles reference value registration independently in CASE 3.
;;------------------------------------------------------------------
NEW isReg ; holds the registered/not-registered result returned to caller
; val is registered if a ^TBDR node exists for it under this attribute
SET isReg=($GET(^TBDR(aid,val))'="")
; Debug trace, left commented, for tracing why a value was found unregistered
; IF 'isReg DO LOGDEBUG^logger("sapiVld","IsRangeValue: not registered — aid="_aid_", val="_val)
QUIT isReg
== L6 ResolveKeyLabel^sapiRslv
ResolveKeyLabel(aid,refeid,valkey,modelType) ; resolve an ordinary key label to an eid and register
;;------------------------------------------------------------------
;; Function : ResolveKeyLabel^sapiRslv
;;
;; Call : SET result=$$ResolveKeyLabel^sapiRslv(aid,refeid,.valkey)
;;
;; Usage : Private helper for ResolveAttrRef^sapiRslv
;;
;; Purpose : Resolves an ordinary key label (e.g. a sys.attr.key alias)
;; to an eid via GetEID^apiGet or GetEID^sapiGet and registers the value key.
;;
;; Parameters
;; aid : (IN) Reference-typed attribute entity ID
;; refeid : (IN) Ordinary key label to resolve
;; modelType : (IN) Model layer containing the entity "ABox" or "TBox"
;; valkey : (OUT) 1 new / 0 existing value key, or -1 if refeid is invalid or does not exist
;;
;;
;; Notes : valkey (by reference) is populated by RegisterAttrVal^sapiRslv.
;;------------------------------------------------------------------
NEW eid ; eid resolved from refeid, or <=0 if not found
NEW result ; status code to return
NEW msg ; debug log message text
NEW RFailed ; flag to return failure
SET RFailed=-1
IF modelType="ABox" SET eid=$$GetEID^apiGet(refeid)
ELSE SET eid=$$GetEID^sapiGet(refeid)
IF eid>0 DO ; refeid resolved to an existing eid
. SET msg="ResolveKeyLabel: resolved entity key '"_refeid_"' -> eid="_eid_" for aid="_aid
. DO LOGDEBUG^logger("sapiRslv",msg)
. SET result=$$RegisterAttrVal^sapiRslv(aid,eid,.valkey)
ELSE SET result=RFailed ; refeid did not resolve to an eid
QUIT result ; new/existing/failed status
== L6 ResolveRawEid^sapiRslv
ResolveKeyLabel(aid,refeid,valkey,modelType) ; resolve an ordinary key label to an eid and register
;;------------------------------------------------------------------
;; Function : ResolveKeyLabel^sapiRslv
;;
;; Call : SET result=$$ResolveKeyLabel^sapiRslv(aid,refeid,.valkey)
;;
;; Usage : Private helper for ResolveAttrRef^sapiRslv
;;
;; Purpose : Resolves an ordinary key label (e.g. a sys.attr.key alias)
;; to an eid via GetEID^apiGet or GetEID^sapiGet and registers the value key.
;;
;; Parameters
;; aid : (IN) Reference-typed attribute entity ID
;; refeid : (IN) Ordinary key label to resolve
;; modelType : (IN) Model layer containing the entity "ABox" or "TBox"
;; valkey : (OUT) 1 new / 0 existing value key, or -1 if refeid is invalid or does not exist
;;
;;
;; Notes : valkey (by reference) is populated by RegisterAttrVal^sapiRslv.
;;------------------------------------------------------------------
NEW eid ; eid resolved from refeid, or <=0 if not found
NEW result ; status code to return
NEW msg ; debug log message text
NEW RFailed ; flag to return failure
SET RFailed=-1
IF modelType="ABox" SET eid=$$GetEID^apiGet(refeid)
ELSE SET eid=$$GetEID^sapiGet(refeid)
IF eid>0 DO ; refeid resolved to an existing eid
. SET msg="ResolveKeyLabel: resolved entity key '"_refeid_"' -> eid="_eid_" for aid="_aid
. DO LOGDEBUG^logger("sapiRslv",msg)
. SET result=$$RegisterAttrVal^sapiRslv(aid,eid,.valkey)
ELSE SET result=RFailed ; refeid did not resolve to an eid
QUIT result ; new/existing/failed status
L5 RegisterAttrVal^sapiRslv
RegisterAttrVal(aid,value,valkey) ; register the value key for (aid,value)
;;--------------------------------------------------------------------------------------
;; Function : RegisterAttrVal^sapiRslv
;;
;; Call : SET isNewValKey=$$RegisterAttrVal^sapiRslv(aid,value,.valkey)
;;
;; Usage : Shared register allocator for both literal and reference-typed
;; attribute values. Literal values are passed in as-is; reference values
;; must already be resolved to an eid by the caller (ResolveKeyLabel^sapiRslv /
;; ResolveRawEid^sapiRslv) before reaching this routine.
;;
;; Parameters
;; aid : (IN) Attribute entity ID, pre-resolved by caller. Must be a
;; literal-typed attribute — e.g. 210 for "sandbox.movie.title".
;;
;; value : (IN) Literal value or reference
;; valkey : (OUT)
;;
;;
;; Notice : Common to both TBox and ABox. Called by DatomAssert^sapiWFL,
;; DatomAssert^apiWFL, and DatomRetract^apiWFL
;;
;; Returns :
;; 1 : new value key allocated (CASE 1)
;; 0 : existing value key reused (CASE 2)
;; -1 : resolution failed valkey is left ""
;;------------------------------------------------------------------
NEW isNewValKey ; result returned to caller
NEW RFailed ; Resolve Failed
NEW RNew ; Resolve New
NEW RExists ; Resolve Existing
SET RFailed=-1
SET RNew=1
SET RExists=0
SET valkey="" ; this is the value it takes if the function fails to resolve the reference
SET isNewValKey=RExists
; Is this a new value for this attribute? If so, allocate a new key and register it.
IF $$IsNewRangeValue^sapiVld(aid,value) DO QUIT isNewValKey
. SET valkey=$$CreateNewValKey(aid,value)
. IF valkey<0 DO
. . DO LOGERROR^logger("sapiRslv","RegisterAttrVal: failed allocating value key for aid="_aid_", value="_value)
. . SET isNewValKey=RFailed
. ELSE DO
. . SET isNewValKey=RNew
. . DO LOGDEBUG^logger("sapiRslv","RegisterAttrVal: (RES)-NEW ^TBDR("_aid_","_value_") -> "_valkey)
; Is this value already registered for this attribute? If so, reuse the existing key.
IF $$IsRangeValue^sapiVld(aid,value) DO QUIT isNewValKey
. SET valkey=$$GetValKey^sapiGet(aid,value)
. IF valkey="" DO
. . DO LOGERROR^logger("sapiRslv","RegisterAttrVal: ^TBDR index missing aid="_aid_", value="_value)
. . SET isNewValKey=RFailed
. ELSE DO
. . DO LOGDEBUG^logger("sapiRslv","RegisterAttrVal: (RES)-EXISTS ^TBDR("_aid_","_value_") -> "_valkey)
; SAFETY NET — should be unreachable now that IsNewRangeValue/IsRangeValue are
; exact complements. If this ever fires, that invariant has been broken again;
; fail loudly rather than silently returning RExists with an empty valkey.
DO LOGERROR^logger("sapiRslv","RegisterAttrVal: neither new nor existing — invariant broken, aid="_aid_", value="_value)
SET isNewValKey=RFailed
QUIT isNewValKey
== L6 RegisterAttrVal^sapiRslv
RegisterAttrVal(aid,value,valkey) ; register the value key for (aid,value)
;;------------------------------------------------------------------------------------------
;; Function : RegisterAttrVal^sapiRslv
;;
;; Call : SET isNewValKey=$$RegisterAttrVal^sapiRslv(aid,value,.valkey)
;;
;; Usage : Shared register allocator for both literal and reference-typed
;; attribute values. Literal values are passed in as-is; reference values
;; must already be resolved to an eid by the caller (ResolveKeyLabel^sapiRslv /
;; ResolveRawEid^sapiRslv) before reaching this routine.
;;
;; Parameters
;; aid : (IN) Attribute entity ID, pre-resolved by caller. Must be a
;; literal-typed attribute — e.g. 210 for "sandbox.movie.title".
;;
;; value : (IN) Literal value or reference
;; valkey : (OUT)
;;
;;
;; Notice : Common to both TBox and ABox. Called by DatomAssert^sapiWFL,
;; DatomAssert^apiWFL, and DatomRetract^apiWFL
;;
;; Returns :
;; 1 : new value key allocated (CASE 1)
;; 0 : existing value key reused (CASE 2)
;; -1 : resolution failed valkey is left ""
;;------------------------------------------------------------------
NEW isNewValKey ; result returned to caller
NEW RFailed ; Resolve Failed
NEW RNew ; Resolve New
NEW RExists ; Resolve Existing
SET RFailed=-1
SET RNew=1
SET RExists=0
SET valkey="" ; this is the value it takes if the function fails to resolve the reference
SET isNewValKey=RExists
; Is this a new value for this attribute? If so, allocate a new key and register it.
IF $$IsNewRangeValue^sapiVld(aid,value) DO QUIT isNewValKey
. SET valkey=$$CreateNewValKey(aid,value)
. IF valkey<0 DO
. . DO LOGERROR^logger("sapiRslv","RegisterAttrVal: failed allocating value key for aid="_aid_", value="_value)
. . SET isNewValKey=RFailed
. ELSE DO
. . SET isNewValKey=RNew
. . DO LOGDEBUG^logger("sapiRslv","RegisterAttrVal: (RES)-NEW ^TBDR("_aid_","_value_") -> "_valkey)
; Is this value already registered for this attribute? If so, reuse the existing key.
IF $$IsRangeValue^sapiVld(aid,value) DO QUIT isNewValKey
. SET valkey=$$GetValKey^sapiGet(aid,value)
. IF valkey="" DO
. . DO LOGERROR^logger("sapiRslv","RegisterAttrVal: ^TBDR index missing aid="_aid_", value="_value)
. . SET isNewValKey=RFailed
. ELSE DO
. . DO LOGDEBUG^logger("sapiRslv","RegisterAttrVal: (RES)-EXISTS ^TBDR("_aid_","_value_") -> "_valkey)
; SAFETY NET — should be unreachable now that IsNewRangeValue/IsRangeValue are
; exact complements. If this ever fires, that invariant has been broken again;
; fail loudly rather than silently returning RExists with an empty valkey.
DO LOGERROR^logger("sapiRslv","RegisterAttrVal: neither new nor existing — invariant broken, aid="_aid_", value="_value)
SET isNewValKey=RFailed
QUIT isNewValKey
L5 IsDatomRecorded^apiVld
IsDatomRecorded(eid,aid,tx,valkey) ; check whether a datom is recorded
;;------------------------------------------------------------------------------------------------
;; Function : IsDatomRecorded^apiVld
;;
;; Call : WRITE $$IsDatomRecorded^apiVld(eid,aid,tx,valkey)
;;
;; Purpose : Determine whether a datom transaction record exists.
;;
;; Parameters
;; eid : (IN) Entity identifier.
;; aid : (IN) Attribute identifier.
;; tx : (IN) Transaction identifier.
;; valkey : (IN) Value key.
;;
;; Scope
;; Reads : ^EATV(eid,aid,tx,valkey)
;;
;; Returns : 1 if the datom record exists, otherwise 0.
;;------------------------------------------------------------------------------------------------
; Check the primary datom store.
QUIT $DATA(^EATV(eid,aid,tx,valkey))>0 ; Return record existence
L3 DatomAssertSingle
DatomAssertSingle(eid,aid,val) ; resolve and stage a single-value (cardinality-one) datom
;;----------------------------------------------------------------------------------------------
;; Function : DatomAssertSingle^apiWFL
;;
;; Purpose : Resolves val to a valkey, enforces the uniqueness constraint, and stages the
;; Revise operation for the entity's current cardinality-one value via DatomRevise.
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID of the assertion predicate — must be cardinality-one
;; val : (IN) Literal or reference value — must not contain "|"
;;
;; Returns
;; 2 : no-op — value already current, nothing staged
;; 1 : triplet staged successfully
;; 0 : triplet assertion failed — bad delimiter, resolution failure, or uniqueness violation
;;----------------------------------------------------------------------------------------------
NEW isAllOk ; overall success flag returned to caller
NEW valkey ; resolved value-type key for val
NEW isNewValKey ; resolution result — 1 new valkey allocated, 0 existing valkey reused, <0 resolution failure
SET isAllOk=1
SET valkey=""
; Reject "|" outright — never silently store a pipe-delimited literal as a single value.
IF val["|" DO QUIT 0
. DO LOGERROR^logger("apiWFL","DatomAssertSingle: '|' delimiter not allowed for cardinality-one attribute ("_aid_") — val="_val)
; Resolve into valkey. Reference-typed attributes resolve/register via ResolveAttrRef,
; everything else registers as a literal via RegisterAttrVal.
IF $$IsRefValType^sapiVld(aid) DO
. SET isNewValKey=$$ResolveAttrRef^sapiRslv(aid,val,.valkey,"ABox")
ELSE DO
. SET isNewValKey=$$RegisterAttrVal^sapiRslv(aid,val,.valkey)
; Resolution failure (-1) must abort this datom immediately — the resolver has already
; logged the specific cause; this just enforces the abort.
IF isNewValKey<0 DO QUIT 0
. DO LOGERROR^logger("apiWFL","DatomAssertSingle: aborting — value resolution failed for (eid="_eid_",aid="_aid_",val="_val_")")
; Uniqueness constraint — failed abort immediately, skipping DatomRevise
IF '$$ValidateUniqueConstraint^apiVld(eid,aid,val,valkey,isNewValKey) QUIT 0
; Apply the revise operation to stage the datom
SET isAllOk=$$DatomRevise(eid,aid,valkey)
QUIT isAllOk
L4 ValidateUniqueConstraint^apiVld
ValidateUniqueConstraint(eid,aid,val,valkey,isNewValKey) ; validate insert/upsert uniqueness
;;------------------------------------------------------------------
;; Function : ValidateUniqueConstraint^apiVld
;;
;; Call : SET ok=$$ValidateUniqueConstraint^apiVld
;;
;; Usage : Called after value resolution and before datom staging
;;
;; Purpose : Enforce unique attribute ownership
;;
;; Parameters
;; eid : (IN) Entity asserting the value
;; aid : (IN) Attribute identifier
;; val : (IN) Original caller supplied value
;; valkey : (IN) Resolved value key
;; isNewValKey : (IN) Value resolution status
;;
;; Returns : 1 if uniqueness validation succeeds, otherwise 0
;;
;; Notes :
;;------------------------------------------------------------------
NEW uniqueMode ; Attribute uniqueness mode
NEW ownereid ; Existing live owner of valkey, 0/"" if none
SET ownereid=""
SET uniqueMode=$$GetUniqueMode^sapiGet(aid)
; Live-owner lookup is only required for reused value keys.
; ResolveLiveEID already applies liveness — historical (retracted)
; ownership alone yields no live owner, matching the "not a violation" rule.
IF (uniqueMode=UNQINSERTID)!(uniqueMode=UNQUPSERTID),'isNewValKey DO
. SET ownereid=$$ResolveLiveEID^apiRslv(aid,val)
; Reject only when another entity currently owns the value.
IF ownereid'="",ownereid'=0,(ownereid'=eid) DO QUIT 0
. IF uniqueMode=UNQINSERTID DO
. . DO LOGERROR^logger("apiVld","DatomAssert: insert-unique violation — ("_aid_","_val_":"_valkey_") already owned by ("_ownereid_")")
. ELSE DO
. . DO LOGERROR^logger("apiVld","DatomAssert: upsert-unique collision — ("_aid_","_val_":"_valkey_") already owned by ("_ownereid_")")
; Uniqueness constraint satisfied.
QUIT 1
L4 ResolveLiveEID^apiRslv
GetCurrentVK(eid,aid)
;;------------------------------------------------------------------
;; Function : GetCurrentVK^apiGet
;;
;; Purpose : Returns the currently active valkey for a given (eid,aid)
;; pair by reading ^EATV at the most recent transaction.
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID
;;
;; Returns :
;; valkey — current valkey (fact is currently TRUE)
;;
;; 0 — valkey found but its most recent state is a retraction (fact is currently FALSE)
;;
;; -1 — valkey not found for an entity with this attribute, i.e. for this (eid,aid) pair
;;------------------------------------------------------------------
NEW curtx,vk1,vk2
; Find the most recent tx for this (eid,aid) — O(1) via descending $ORDER
SET curtx=$ORDER(^EATV(eid,aid,""),-1)
; valkey not found for an entity with this attribute, i.e. for this (eid,aid) pair
IF curtx="" DO QUIT -1 ; return -1
. DO LOGDEBUG^logger("apiGet","GetCurrentVK: valkey Not found for ("_eid_","_aid_")")
; The datom (eid,aid,tx,vk) has been set currently set to TRUE a revision or assertion
SET vk1=$ORDER(^EATV(eid,aid,curtx,""))
IF $GET(^EATV(eid,aid,curtx,vk1))=1 QUIT vk1 ; return the valkey
; The valkey has been currently retracted, i.e. the datom (eid,aid,tx,vk) has been set to FALSE
SET vk2=$ORDER(^EATV(eid,aid,curtx,vk1))
IF vk2="" DO QUIT 0 ; return 0
. DO LOGDEBUG^logger("apiGet","GetCurrentVK: retracted, ("_eid_","_aid_") is currently false")
QUIT vk2
L4 DatomRevise^apiWFL
DatomRevise(eid,aid,valkey) ; stage the cardinality-one Revise operation for (eid,aid) → valkey
;;------------------------------------------------------------------
;; Function : DatomRevise^apiWFL
;;
;; Purpose : Implements the Revise compound operation for a cardinality-one (eid,aid) pair —
;; retracts the currently active value at t1 and asserts the new valkey at t2,
;; by staging the appropriate datoms into %ABR.
;;
;; Three paths depending on current state of (eid, aid):
;;
;; ABSENT (no active datom)
;; → stage assert (OP=1) for new valkey INSERT
;;
;; ACTIVE, same valkey
;; → no-op, nothing staged NO-OP
;;
;; ACTIVE, different valkey
;; → stage retract (OP=0) for old valkey REPLACE
;; stage assert (OP=1) for new valkey
;; (both in same transaction)
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID (must be cardinality-one)
;; valkey : (IN) Resolved value key for the new assertion
;;
;; Returns :
;; 2 — no-op (value already current, nothing to stage)
;; 1 — datom(s) staged (INSERT or REPLACE path)
;; 0 — failure (reserved for future error conditions)
;;------------------------------------------------------------------
NEW curvk
SET curvk=$$GetCurrentVK^apiGet(eid,aid)
; Valkey is not found or it has been previously retracted
IF curvk=-1 DO QUIT 1
. SET %ABR(eid,aid,valkey)=1
. DO LOGDEBUG^logger("apiWFL","DatomRevise: insert ("_eid_","_aid_","_valkey_") — (eid,aid) not found")
IF curvk=0 DO QUIT 1
. SET %ABR(eid,aid,valkey)=1
. DO LOGDEBUG^logger("apiWFL","DatomRevise: insert ("_eid_","_aid_","_valkey_") — prior datom retracted")
; Same valkey with the current valkey — no-op
IF curvk=valkey DO QUIT 2
. DO LOGDEBUG^logger("apiWFL","DatomRevise: no-op ("_eid_","_aid_","_valkey_") — value unchanged")
; Different valkey from the current valkey — retract old, assert new (revision operation)
SET %ABR(eid,aid,curvk)=0
SET %ABR(eid,aid,valkey)=1
DO LOGDEBUG^logger("apiWFL","DatomRevise: replace ("_eid_","_aid_","_curvk_") → ("_valkey_")")
QUIT 1
L5 GetCurrentVK^apiGet
GetCurrentVK(eid,aid)
;;------------------------------------------------------------------
;; Function : GetCurrentVK^apiGet
;;
;; Purpose : Returns the currently active valkey for a given (eid,aid)
;; pair by reading ^EATV at the most recent transaction.
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID
;;
;; Returns :
;; valkey — current valkey (fact is currently TRUE)
;;
;; 0 — valkey found but its most recent state is a retraction (fact is currently FALSE)
;;
;; -1 — valkey not found for an entity with this attribute, i.e. for this (eid,aid) pair
;;------------------------------------------------------------------
NEW curtx,vk1,vk2
; Find the most recent tx for this (eid,aid) — O(1) via descending $ORDER
SET curtx=$ORDER(^EATV(eid,aid,""),-1)
; valkey not found for an entity with this attribute, i.e. for this (eid,aid) pair
IF curtx="" DO QUIT -1 ; return -1
. DO LOGDEBUG^logger("apiGet","GetCurrentVK: valkey Not found for ("_eid_","_aid_")")
; The datom (eid,aid,tx,vk) has been set currently set to TRUE a revision or assertion
SET vk1=$ORDER(^EATV(eid,aid,curtx,""))
IF $GET(^EATV(eid,aid,curtx,vk1))=1 QUIT vk1 ; return the valkey
; The valkey has been currently retracted, i.e. the datom (eid,aid,tx,vk) has been set to FALSE
SET vk2=$ORDER(^EATV(eid,aid,curtx,vk1))
IF vk2="" DO QUIT 0 ; return 0
. DO LOGDEBUG^logger("apiGet","GetCurrentVK: retracted, ("_eid_","_aid_") is currently false")
QUIT vk2
L2 DatomRetract^apiWFL
DatomRetract(eid,aid,val) ; dispatch to the single- or multi-value retraction branch by attribute cardinality
;;---------------------------------------------------------------------------------------------
;; Function : DatomRetract^apiWFL
;;
;; Purpose : Normalizes val, then dispatches to DatomRetractSingle or DatomRetractMulti based
;; on the attribute's cardinality.
;;
;; Parameters
;; eid : (IN) Entity ID
;;
;; aid : (IN) Attribute ID
;;
;; val : (IN, must be omitted for cardinality-one)
;; (IN, required for cardinality-many) Value to retract
;;
;; Returns
;; 1 : triplet retraction staged
;; 0 : triplet retraction failed
;;--------------------------------------------------------------------------------------------
SET val=$GET(val)
IF $$IsMultiValue^sapiVld(aid) QUIT $$DatomRetractMulti(eid,aid,val)
QUIT $$DatomRetractSingle(eid,aid,val)
L3 DatomRetractMulti^apiWFL
DatomRetractMulti(eid,aid,val) ; recurse per "|" token, then stage each via DatomStageMulti
;;----------------------------------------------------------------------------------------------
;; Function : DatomRetractMulti^apiWFL
;;
;; Purpose : Validates val is supplied, splits "|"-delimited val into independent
;; DatomRetractMulti calls, or — for a single token — delegates to
;; DatomStageMulti with op=0.
;;
;; Parameters
;; eid : (IN) Entity ID
;; aid : (IN) Attribute ID — must be cardinality-many
;; val : (IN) Value(s) to retract — required, can be multi-value "val1|val2"
;;
;; Returns
;; 2 : no-op — single token only, value already inactive/retracted
;; 1 : triplet(s) staged successfully
;; 0 : one or more retractions failed — val omitted, resolution failed, or never recorded
;;----------------------------------------------------------------------------------------------
IF val="" DO QUIT 0
. DO LOGERROR^logger("apiWFL","DatomRetractMulti: value required for cardinality-many attribute ("_aid_")")
NEW isAllOk ; overall success flag returned to caller — cleared if any recursive/staging step fails
NEW pIdx ; 1-based piece index while splitting val on "|"
NEW pieceVal ; current "|"-delimited token during multi-value recursion
SET isAllOk=1
; Delimiter present — recurse per "|"-delimited token as an independent datom retraction.
IF val["|" DO QUIT isAllOk
. FOR pIdx=1:1 SET pieceVal=$PIECE(val,"|",pIdx) QUIT:pieceVal=""!'isAllOk DO
. . IF '$$DatomRetractMulti(eid,aid,pieceVal) SET isAllOk=0
QUIT $$DatomStageMulti(eid,aid,val,0)