BatchResult.IDs: read the identifiers out of a batch without losing their positions - #18
Merged
Conversation
"Create n things and get n ids" is the batch a real integration writes,
and unpacking it is the same loop every time: walk Order, call Get,
unmarshal into an ID, cope with the ones that failed. The coping is the
interesting part, and it is the part that gets skipped at the call site.
IDs always returns one entry per command, positionally aligned with
Order, so the alignment with whatever the caller built the batch from
survives. A command that produced no identifier leaves a zero rather
than shortening the slice: dropping the gaps would slide every later id
onto the wrong entity, and that is not an error anywhere — the ids are
all real, they are just attached to the wrong things, and the next
update writes to whichever entity it lands on.
The error names every gap and wraps the underlying failures, so
errors.Is and errors.As reach a command's *APIError. It comes back
alongside the ids rather than instead of them: the commands that
succeeded have already committed on the portal.
A result of the wrong shape is a gap with its answer quoted, not a
plausible 0 — crm.deal.update answers true and crm.item.add wraps the id
in {"item":{...}}. So is a command that answered null or "": ID reads
those as an unset FIELD, which is a normal Bitrix24 answer, but as a
whole result it means nothing was identified.
Verified on a live portal: 50 crm.deal.add in one batch gave 50 non-zero
ids in submission order, a batch of update+get gave two zeros and an
error naming both; everything created was deleted by those same ids and
the deletion confirmed by reading them back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
A live run showed that "create N entities and get N identifiers back" is the most common batch there is, and reading its result out is the same every time. Walk
Order, callGet, unmarshal into ab24.ID, cope somehow with the commands that failed. That last part is the interesting one, and it is exactly the part a call site usually skips.What a partial success returns
A slice as long as
Order, always, positionally aligned with it. A command that produced no identifier leaves a zero in its place (ID.IsZero) rather than dropping out of the slice.That is the decision that matters. Dropping the gaps would shift every identifier after them onto the wrong entities — and that is nowhere an error: the identifiers are real, they simply belong to something else, and the next
updatewrites wherever it landed.The error names every gap and wraps the underlying ones, so
errors.Is/errors.Asreach the*APIErrorof the command that failed. It is returned alongside the identifiers: the commands that succeeded have already committed on the portal, and a caller that drops the slice has no way to learn what exists.What counts as a gap
What is decoded is a result that is itself an identifier — that is how the classic
*.addmethods answer. A gap is left by:Halt(these are different texts in the error);truefromcrm.deal.update,{"item":{"id":…}}fromcrm.item.add— reported with the response quoted, rather than turned into a plausible0;nullor""response.b24.IDdecodes those into a zero deliberately — an unset identifier is a normal Bitrix24 answer for a field. As a whole result it means nothing was identified, and a silent zero would be the one gap with no error next to it — precisely the one that later travels into adelete.Checks
go build ./...go vet ./...gofmt -l .— no outputgo test -race ./...Compatibility
Tests
batch_ids_test.go:TestBatchIDsComeBackInSubmissionOrder— the identifiers of commandsz,m,aare deliberately sorted the other way round: a slice assembled by ranging over the result maps would come back reversed, and nobody would notice — both orders look like a list of plausible identifiers.TestBatchIDsKeepAFailedCommandsPosition— a failing command in the middle:[11 0 13], the third identifier has not slid into the hole; the error reaches*APIErrorthrougherrors.As.TestBatchIDsAcceptBothWireSpellings— a number and a string in one batch.TestBatchIDsRefuseAResultThatIsNotAnIdentifier—trueand a wrapped object.TestBatchIDsRefuseAnEmptyIdentifier—nulland"".TestBatchIDsReportACommandThatNeverRan—Halt.TestBatchIDsOnNilResult— a nil receiver, as everywhere else in the SDK.Verified on a live portal:
Everything created was deleted with those same identifiers and its absence verified by reading; the portal again holds exactly the 54 deals it held before the run.