Skip to content

build_entities_from_data: stop a null object from corrupting the entities - #48

Open
seebi wants to merge 2 commits into
mainfrom
feature/entity-builder-null-objects
Open

seebi wants to merge 2 commits into
mainfrom
feature/entity-builder-null-objects

Conversation

@seebi

@seebi seebi commented Sep 15, 2026

Copy link
Copy Markdown
Member

Two defects in build_entities_from_data, both reached by the same ordinary data: a key
that one item of a list carries as an object and another as null. Found while writing a
GraphQL response to a JSON dataset - a GitLab query asking for the user who created a
Terraform state version hits it as soon as one version was created by a job rather than a
person - and reproduced against a running deployment.

Each commit is one defect, and the second is the more invasive of the two, so it can be
dropped on its own if you would rather ship only the first.

1. A relation whose value is null got [""]

A relation path carries URIs pointing at sub entities, but a key the data has as null was
given [""] like any other missing value. That empty string is a reference to a sub
entity that does not exist. A consumer that follows it finds nothing: writing such
entities to a JSON dataset aborts the whole write with

Current context not Array but Object

before a single entity is written. The path is now left empty instead. Only relations
change - a plain path keeps its [""], which is what a dataset writes as an empty value.

2. The type of a key was taken from the last item seen

The per-key type maps were merged with |, so of two items that disagree about a key the
last one decided the schema. In one of the two orders that loses information:

[{"name": "sai", "address": {"city": "San Francisco"}},
 {"name": "seebi", "address": None}]

address ends up typed NoneType, which is not a relation, so the object in the first
item is flattened into its str() and the entity carries the text
{'city': 'San Francisco'} instead of a reference. Reversing the two items produces a
different schema from the same kind of data. A list of objects followed by an empty list
behaves the same way.

The merge now keeps the type that says more: NoneType loses to anything, and a bare
list loses to a list_dict. Genuinely conflicting types, say str against int, are
still resolved by the last item seen, as before.

This changes the entities built from such data: a key that used to come out as text
now comes out as a relation with sub entities behind it. That is the point of the fix, but
it is a shape change for anyone whose mapping consumed the flattened text.

Tests

Three regression tests, one per symptom - the null relation, the object followed by a
null, and the list of objects followed by an empty list. Each fails on main and passes
here; I checked by reverting the source with the tests in place.

task check is green: ruff, mypy across 51 files, deptry, trivy, 55 passed 1 skipped.

🤖 Generated with Claude Code

seebi and others added 2 commits September 15, 2026 23:59
A relation path carries URIs pointing at sub entities, but a key the data has as
null was given [""] like any other missing value. That empty string is a
reference to a sub entity that does not exist, so a consumer that follows it
finds nothing: writing such entities to a JSON dataset aborts the whole write
with "Current context not Array but Object", before a single entity is written.

Reaching it needs one item of a list to carry the object and another to have it
as null, which is ordinary in an API response - a GitLab query asking for the
user who created a Terraform state version hits it as soon as one version was
created by a job rather than a person.

Only relations change. A plain path keeps its [""], which is what a dataset
writes as an empty value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A key's type is read off the values in the data, and two items can disagree
about it: one object carries an address and the next has it as null, or one
list is empty and the next holds objects. The type maps were merged with
`|`, so whichever item came last decided the schema.

That lost information in one of the two orders. A key seen as an object and
then as null ended up typed NoneType, which is not a relation, so the object
in the first item was flattened into its str() - the entity carried the text
{'city': 'San Francisco'} instead of a reference to a sub entity. The same
happened to a list of objects followed by an empty list. Reversing the two
items produced a different schema from the same kind of data.

The merge now keeps the type that says more: NoneType loses to anything, and
a bare list loses to a list of objects. Genuinely conflicting types, say str
against int, are still resolved by the last item seen, as before.

This changes the entities built from such data: a key that used to come out
as text now comes out as a relation with sub entities behind it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage

Coverage Report
File Stmts Miss Cover Missing
init.py 0 0 100%
testing.py 65 6 91% 74 124 140 144 162 166
dataintegration/init.py 0 0 100%
dataintegration/client.py 7 0 100%
dataintegration/context.py 57 0 100%
dataintegration/description.py 182 16 91% 46 73-74 146-147 151 161 166 180 186 227 241 365 408 416-417
dataintegration/discovery.py 49 36 27% 27 38-43 55-62 64 66-68 70 84-85 87-90 92-93 95-100 106 108
dataintegration/entity.py 31 3 90% 41 66 86
dataintegration/plugins.py 30 5 83% 23 27 31 35 50
dataintegration/ports.py 15 1 93% 32
dataintegration/types.py 115 13 89% 70-71 87 99 107 120 155 159-161 171 175 276
dataintegration/parameter/init.py 0 0 100%
dataintegration/parameter/choice.py 23 1 96% 26
dataintegration/parameter/code.py 52 0 100%
dataintegration/parameter/dataset.py 29 4 86% 27 32 52 55
dataintegration/parameter/graph.py 55 3 95% 63 109-110
dataintegration/parameter/multiline.py 4 4 0% 3 6 9-10
dataintegration/parameter/password.py 28 13 54% 11-13 17 34-37 39-40 53-55
dataintegration/parameter/resource.py 15 0 100%
dataintegration/typed_entities/init.py 6 0 100%
dataintegration/typed_entities/file.py 153 29 81% 96 100-101 105-106 110-111 115 118 121 222 245-246 279 312 314 339-344 349 382-387
dataintegration/typed_entities/quads.py 99 57 42% 90-106 135-139 142-146 149 159 162 179-186 189 192-194 196 199 202-208 210 213-216 219
dataintegration/typed_entities/typed_entities.py 28 2 93% 56-57
dataintegration/utils/init.py 20 2 90% 58 60
dataintegration/utils/entity_builder.py 94 0 100%
TOTAL 1157 195 83%  

Tests Skipped Failures Errors Time
56 1 💤 0 ❌ 0 🔥 30.943 ⏱

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