Skip to content

BatchResult.IDs: read the identifiers out of a batch without losing their positions - #18

Merged
ExaltedTrou6 merged 1 commit into
mainfrom
feat/batch-ids
Aug 7, 2026
Merged

BatchResult.IDs: read the identifiers out of a batch without losing their positions#18
ExaltedTrou6 merged 1 commit into
mainfrom
feat/batch-ids

Conversation

@ExaltedTrou6

@ExaltedTrou6 ExaltedTrou6 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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, call Get, unmarshal into a b24.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.

res, err := client.Core().CallBatchChunked(ctx, b)
ids, idErr := res.IDs()   // ids[i] is the identifier of the i-th command ADDED

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 update writes wherever it landed.

The error names every gap and wraps the underlying ones, so errors.Is / errors.As reach the *APIError of 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 *.add methods answer. A gap is left by:

  • a command that failed on the portal, and a command that never ran because of Halt (these are different texts in the error);
  • a response of another shape: true from crm.deal.update, {"item":{"id":…}} from crm.item.add — reported with the response quoted, rather than turned into a plausible 0;
  • a null or "" response. b24.ID decodes 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 a delete.

Checks

  • go build ./...
  • go vet ./...
  • gofmt -l . — no output
  • go test -race ./...

Compatibility

  • The change is additive: existing signatures are untouched

Tests

batch_ids_test.go:

  • TestBatchIDsComeBackInSubmissionOrder — the identifiers of commands z, m, a are 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 *APIError through errors.As.
  • TestBatchIDsAcceptBothWireSpellings — a number and a string in one batch.
  • TestBatchIDsRefuseAResultThatIsNotAnIdentifiertrue and a wrapped object.
  • TestBatchIDsRefuseAnEmptyIdentifiernull and "".
  • TestBatchIDsReportACommandThatNeverRanHalt.
  • TestBatchIDsOnNilResult — a nil receiver, as everywhere else in the SDK.

Verified on a live portal:

50 x crm.deal.add: wall=28.47s in 1 HTTP request(s)
IDs() -> 50 ids, first=4309 last=4407, all non-zero=true
submission order preserved (ids ascending): true

mixed batch IDs() -> [0 0]
  err: b24gosdk: batch: "upd" (crm.deal.update) answered true, which is not an identifier; read it with Get
       b24gosdk: batch: "nope" (crm.deal.get) answered {"ID":"4311","TITLE":"…","TYPE_ID":"SALE","STA…, which is not an identifier; read it with Get

cleaning up 50 deals
deleted: 50 ok, 0 failed
verification: 0 of 50 still on the portal (50 gone)

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.

"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>
@ExaltedTrou6 ExaltedTrou6 reopened this Aug 7, 2026
@ExaltedTrou6
ExaltedTrou6 merged commit 0a6368a into main Aug 7, 2026
3 of 4 checks passed
@ExaltedTrou6 ExaltedTrou6 changed the title BatchResult.IDs: забрать идентификаторы из батча, не потеряв позиции BatchResult.IDs: read the identifiers out of a batch without losing their positions Aug 7, 2026
@ExaltedTrou6
ExaltedTrou6 deleted the feat/batch-ids branch August 7, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant