Observed
Booting an app whose seed declares a composite externalId (here ['employer', 'user'] on ats_employer_member), every per-record failure line the loader prints carries a raw NUL byte:
ERROR [SeedLoader] Failed to write ats_employer_member record #0 (employer+user=ats_employer-1788753956811-1<U+0000>usr_ats_quillstone_admin): the data engine rejected the write; ...
(<U+0000> marks where od -c shows \0 in the captured log.) One such line is enough for grep to classify the whole log as binary (Binary file matches), so every later grep -c / grep -n over that log silently returns nothing until the reader remembers -a. Measured on @objectstack/metadata-protocol 17.3.0 while investigating objectstack-ai/ats#20.
Cause
SeedLoaderService.externalIdKey() deliberately joins composite key parts with "\0" — its comment says so: a separator that cannot occur in a natural-key value, so ('a','b') and ('a\0b','') never collide. That is correct for the KEY. The defect is that the KEY string is what gets interpolated into the human-readable failure message (the (label=value) parenthetical), rather than a display rendering with a visible separator.
- key:
packages/metadata-protocol seed loader, externalIdKey(record, externalId) → parts.join("\0")
- label: the same class's
externalIdLabel(externalId) already renders the FIELD names with + (employer+user); the VALUE side has no such rendering and reuses the key.
Expected
The diagnostic renders the composite value with a visible joiner (e.g. employer+user=ats_employer-…-1 + usr_ats_quillstone_admin, or JSON of the parts), keeping the NUL-joined string for map keys only. No control byte should reach a log line — this repo's own check-nul-bytes doctrine (a raw NUL flips a file to binary for grep) applies to emitted diagnostics as much as to source files.
Repro
Any seed dataset with externalId: ['a', 'b'] whose rows fail to write (a throwing hook is the easiest way); read the server log with grep -n 'Failed to write' — no -a — and observe Binary file matches.
Found while measuring objectstack-ai/ats#20; out of scope there, filed here where the fix lands. No assignee.
Observed
Booting an app whose seed declares a composite
externalId(here['employer', 'user']onats_employer_member), every per-record failure line the loader prints carries a raw NUL byte:(
<U+0000>marks whereod -cshows\0in the captured log.) One such line is enough forgrepto classify the whole log as binary (Binary file matches), so every latergrep -c/grep -nover that log silently returns nothing until the reader remembers-a. Measured on@objectstack/metadata-protocol17.3.0 while investigating objectstack-ai/ats#20.Cause
SeedLoaderService.externalIdKey()deliberately joins composite key parts with"\0"— its comment says so: a separator that cannot occur in a natural-key value, so('a','b')and('a\0b','')never collide. That is correct for the KEY. The defect is that the KEY string is what gets interpolated into the human-readable failure message (the(label=value)parenthetical), rather than a display rendering with a visible separator.packages/metadata-protocolseed loader,externalIdKey(record, externalId)→parts.join("\0")externalIdLabel(externalId)already renders the FIELD names with+(employer+user); the VALUE side has no such rendering and reuses the key.Expected
The diagnostic renders the composite value with a visible joiner (e.g.
employer+user=ats_employer-…-1 + usr_ats_quillstone_admin, or JSON of the parts), keeping the NUL-joined string for map keys only. No control byte should reach a log line — this repo's owncheck-nul-bytesdoctrine (a raw NUL flips a file to binary forgrep) applies to emitted diagnostics as much as to source files.Repro
Any seed dataset with
externalId: ['a', 'b']whose rows fail to write (a throwing hook is the easiest way); read the server log withgrep -n 'Failed to write'— no-a— and observeBinary file matches.Found while measuring objectstack-ai/ats#20; out of scope there, filed here where the fix lands. No assignee.