Skip to content

Let a dimension hold no value instead of refusing it or filing it at 0 - #11

Merged
renatex314 merged 1 commit into
masterfrom
fix/no-value-dimension
Oct 5, 2026
Merged

renatex314 merged 1 commit into
masterfrom
fix/no-value-dimension

Conversation

@renatex314

Copy link
Copy Markdown
Owner

Points 2 and 3 of #10, as agreed in the discussion there. Point 1 (per-device rejection, opt-in) is a separate PR. Intended for 0.10.0, because devices reported without dims behave differently.

Behaviour

A device can hold no value on a dimension: it has no cell in any filter combination that includes that dimension.

  • It still shows on the unfiltered map.
  • It still matches filters on its other dimensions.
  • It matches no filter on this one.
Input capacity dimension values dimension
null / [] no value (0.9.1: 400) no value (0.9.1: 400)
dimension absent no value (was slot 0) slot 0, unchanged

A sentinel such as "none" is no longer needed, and no value takes no slot of the capacity.

Why no slot is reserved

The index already stores devices with no cells; every device in a collection without dimensions is one. So "no value" is just an empty cell list for that dimension's filter combinations. Nothing new is written to snapshots and the format doesn't change.

Changes

  • schema.rs (cells_for): an empty value list, or an absent capacity dimension, now resolves to no value instead of slot 0 or a 400.
  • collection.rs (restore): restore used to fill any device without cells with the defaults. That exists for a device that lost all its cells to a config change. A device saved with no value would have been filed under the first label of a declared dimension on every restart, so restore now fills only a device that had cells and lost them all.
  • geojson.rs: comments only. A GeoJSON null already reached the schema as present-but-empty since 0.9.1.
  • README.md, client README, index.d.ts: describe "no value", and null is now part of the client's dims value type.

Not changed

  • Devices already in slot 0 from an earlier version stay there until they're reported again with dims.
  • [1, null] is still a 422: a malformed value, not an empty one.
  • A report naming only some dimensions still overwrites the others. On a capacity dimension they now become "no value" instead of slot 0, but the previous value is still lost. Keeping the other dimensions' values deserves its own issue; per 0.9.1: um dispositivo com dims vazio recusa o lote inteiro, e multi não tem como expressar "sem valor" #10 it doesn't affect current users, who declare one dimension per collection.

Testing

Four new tests in tests/regressions.rs replace the 0.9.1 test that expected the 400s:

  • a_device_with_no_value_matches_no_filter_on_a_capacity_dimension: compact null and [], absent dims, GeoJSON null, and clearing a value with a patch. The filter for the first value matches only its real device, no slot is used, and [1, null] is still 422.
  • an_empty_multi_value_is_no_client_and_does_not_sink_the_batch: 0.9.1: um dispositivo com dims vazio recusa o lote inteiro, e multi não tem como expressar "sem valor" #10's reproduction. f.clientId=7 returns exactly a and e2.
  • a_declared_dimension_keeps_value_zero_for_an_absent_value: an absent declared dimension still takes label 0, and [] there is no value.
  • a_device_with_no_value_survives_a_snapshot_without_one: a snapshot round-trip with a declared and a capacity dimension.

All four also call verify() to check index invariants. Run against the code before this change, all four fail; the snapshot test also fails with only the restore change reverted.

Run locally in rust:1.98-slim-bookworm, with the same steps as CI:

  • cargo fmt --check and cargo clippy --all-targets --all-features with -D warnings: clean.
  • cargo test --release: all pass.
  • Node client npm test against the new binary: 18 unit, 41 integration and 29 CLI tests pass.

🤖 Generated with Claude Code

0.9.1 stopped `null` and `[]` from filing a device under slot 0 by
refusing them. That fixed #8's leak but left, as #10 reports, no way to
say "this vehicle belongs to no client" on a `multi` dimension short of
a sentinel value -- one that takes a slot of the capacity, shows up as a
filterable value, and has to be agreed by every writer -- and a device
reported without `dims` still landed in slot 0, matching the filter of
whatever value was reported first.

A device can now hold no value on a dimension: it has no cell in any
filter shape that includes it. It still shows unfiltered and under
filters on its other dimensions, and matches no filter on that one. The
index already stores devices with no cells -- every device in a
collection without dimensions is one -- so nothing is reserved and the
snapshot format does not change.

  - `null` or `[]`, on any dimension: no value (was a 400 since 0.9.1)
  - absent, on a `capacity` dimension: no value (was slot 0)
  - absent, on a declared dimension: value 0, unchanged; that label is
    one the collection chose, and it is what a missing `category` has
    always meant

Restore had to learn the difference. It filled any device without cells
with the defaults, to keep a device findable when a config change left
none of its cells valid; a device saved with no value would have been
filed under the first label of a declared dimension on every restart.
It now fills only a device that had cells and lost all of them.

Devices already filed at slot 0 by an earlier version stay there until
they are reported again with `dims`. A `null` inside a list is still a
422: `[1, null]` is a malformed value, not an empty one.

The Node client declares `null` as a dimension value, which the server
now accepts.

Refs #10

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@renatex314
renatex314 merged commit 653664b into master Oct 5, 2026
5 of 6 checks passed
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